Managing complexity in React Javascript applications

React is currently (September 2020) a good way to create client-side browser applications. It scales much better than vanilla JS or jquery. As with most front-end, the main difficulty is managing state and data.

React currently provides the following hooks:

The first rule is don’t use state. Nothing should ever have to use state. All components should be functional:

function App() { return <h2>App</h2> }

Unfortunately you will have to use state if you want your application to do anything. For this, use useState:

function App() {
    const [count, setCount] = React.useState(0)
    const button = <button onClick={() => setCount(c => c + 1)}>Increment</button>
    return <div> {button} You have clicked the button {count} times. </div>
}

Once you are using state, even once, your application will have unmanageable complexity. You must manage it anyway. The best thing you can do is remove your calls to useState and stop using state. But you must use state. What can you do?

  1. Always useState at the lowest level possible. Keep all variables as deep in the component tree as you can. Do not use useContext. If you must pass state down the tree, then pass either count or setCount, not both. If you must pass both, then pass them both together with countState = {count, setCount}. Do not pass both. If you must pass both, then pass them packed into one object. Do not pass both. Your app will become unmanagably complex.

Your app is too complex to manage now. You must manage it. If your component tree is deep then that’s a problem. The component tree must not be deep. You also must not have long, complex components. You must break your code into as many components as possible. You must not let your tree get too deep.

Inevitably, you will find that you have a deep component tree, and that a state is defined at the top level which is used by components in the bottom level. You must not pass too many props down the tree. You must not useContext. You must pass all data down the tree explicitly in props. You must not have too many props. You must not use the ...restProps pattern. You must not let your tree get too deep. You will soon find that you have too many props, too many useStates, you’re passing invalid props, and you’re getting render cycles.

You must not permit render cycles. These happen when you useState in a parent, and pass a setState to its child which triggers a re-render of the parent and therefore the child. The solution to this is to useCallback. You should not have to useCallback. If you’re in a situation where you must useCallback this means that your app has become too complex to manage. It is better to useCallback then to have a render loop, because your code will not run at all if there is a loop.

You’ve found yourself using a Context. You put a Provider at the lowest level possible. Usually the lowest level possible is at the top level. This is because your app is too complex. It is important to only put data in a Context. Do not use a Context. Do not put functions or setState or any reducers in the Context. Any changes to the contextual data will cause a global re-render which is why you must not put setState or a reducer in the Context. Do not use more than one context. Do not use reducers. Do not use state.

Your app is poorly designed so you have a global state which is changed by components deep inside the tree. Pack the setCount, count pair into a countState = {setCount, count} and add it into the Context.Provider with value={countState, otherStuff}. Do not use state in a React application. Do not use state when you write software. Only pull out either setCount or count in a component if you only need to use one; do not pull out the packed object unless you need both of its values.

Always store multiple values in an object; never store multiple named values in an array. Thanks to the object destructuring shorthand, you do not have to type your variable names twice ({x: x, y: y}) to use objects. Do not use fixed-length arrays in your software. They’re fixed-length because they have structure, which means you should be using an object or something even more structured / contractual.

Again, do not use a Context. Your code now has complex state-update logic repeated at different points deep inside. Thanks to hooks, these can be pulled out into separate functions, ordinary JS functions, and the code can be re-used across components. Somehow, this does not work. For some reason, the state updates only seem to work from <App/>. It seems all your data is in one giant state object. Your app has become too complex. You useReducer. Why would you do that? It actually is a pretty helpful pattern. It has this syntax:

const [state, dispatch] = useReducer(reducer, initialArg, init);

The reducer usually has this structure:

function reducer(state, action) {
  switch (action.type) {
    case 'increase':
      return {count: state.count + action.amount};
    case 'decrease':
      return {count: state.count - action.amount};
    case 'multiply':
      return {count: state.count * action.factor};
    default:
      throw new Error();
  }
}

Why would you use a dispatch function? That is a pattern from C in the 80s. There is no reason to use a dispatch function in a modern programming language; we have classes with methods attached, first-class-citizen functions, higher-order functions, and many other design patterns that have been shown with time & experience & reason to be far better. We will use a dispatch. Most of your state has no natural place in any lower-level component; the logic for any action depends on the entire state; the structure of the UI depends on the entire state. Your application is far too complex. The first rule of software is do not write software, and the second rule is do not use state.

You cannot just use javascript. These problems exist in React so you feel React is to blame. React is not to blame; it is absolutely genius and perfectly designed. The problem is you. If you use javascript then the complexity will overwhelm you immediately. Nobody will use a desktop application, and besides it’s no simpler there. You must use the browser and you must use react and use must use state and context and reducers.

The hardest part of using reducers is keeping all your action objects correct as you change your code. Since you decided to use a dispatcher instead of separate functions, your editor does not have argument checking or completion. Most of your calls to dispatch from deeply-nested child components will be incorrect. Your application will not work, not least of all because it is too complex to manage.

  1. It is important to name the action.types well, so that you can remember what they do. Put as much thought into naming them as you would into naming functions. Put as much thought into their arguments and argument names as normal javascript function arguments.

Even if your calls to dispatch are correct, the logic inside the dispatch function will not be correct. Further, since dispatch updates global state and triggers a global re-render, you will have a render loop and your application will not display in the browser window. The tab will not close when the user hits ‘x’ because Chrome’s sandboxing is still not good enough. If the browser is not up to date, then your bug will require them to restart their computer.

  1. Write automatic tests for your react application. It is not possible to automatically test a UI. You should automatically test your UI. You will frequently have to restructure your component tree and your Context state, so your tests will be obsolete by the time the code passes them. So always write tests for your code.

  2. Constantly search your code for calls to dispatch and try to delete most of them. Go line by line through your entire codebase every day and delete as much code as you can. Go through your spec doc weekly and remove as many features and robustness checks as you can. It’s important that your code meets spec and is simple and elegant.