This post will cover managing complex state at a feature level rather than the entire site. React hooks have enabled developers to have cleaner functional components which help to rationalise our component logic with ease.

Take useState hook, it’s one line of code that can be used to manage the state of a component rather than having to create a class component with the addition of boiler code. This is great because we are keeping simple things clear!

However, there are features that are inherently complex as they could have many nested child components and need to alter the state.

What options are there to manage this complexity in React?

Person holding plasma ball Photo by Ramón Salinero on Unsplash

Generally is good practice to keep it simple, which might lead you down the path of passing the setState callback down the stack of components. The downside here is the mutation of state can become difficult to follow. Allowing child components to alter the state directly will make it unclear what the state object should look like. By not having a single source of truth where the mutation is managed then the result could be unexpected and requires an overhead to work out.

It is also worth considering how easy is it to test changes to state. Unit testing state through rendering components can be tricky and takes more time to build compared to pure functions.

Ideally you want to make it easy to follow changes to the component state and create unit tests which give confidence that the functionality works.

To help explain the idea I’ve created a classic Todo app in CodeSandbox.io.

React 16.8 introduced hooks. A hook which is helpful for managing complex state is useReducer, and as the docs say it’s an alternative to useState. This essentially borrows the good concepts from Redux but with less complex setup and boiler code to work with React.

const [todoList, dispatch] = useReducer(toDoReducer, initialState);

We can also use React Context. The benefit of combining useReducer with context is being able to call the dispatch function anywhere down the component tree without passing through props. Preventing the need to follow it through the component tree to find the callback.

const TodosDispatch = React.createContext(null);

function App() {
  const [todoList, dispatch] = useReducer(toDoReducer, initialState);
  return (
    <TodosDispatch.Provider value={dispatch}>
     ...ToDoApp
    </TodosDispatch.Provider>
  );
}

To access the context in child components the React hook useContext can be used.

const dispatch = useContext(TodosDispatch);

This idea is recommended in the React docs, avoid passing callback down

Example React todo app with useReducer and useContext

Key points demoed in the CodeSandbox

You can see how clean and simple the unit tests are for the reducer in the todo.spec.js file. This will give confidence that the logic works as expected when state changes. These unit tests help manage complexity, preventing regression when the reducer is updated to handle a new action.

  • Key functions:
    • TodosContext is where the reducer dispatch callback will be stored
    • toDoReducer transforms the task list state based on actions e.g. add task
    • initialState contains one task object in an Array
    • addAction, markAction, deleteAction are all action creators which describe how to change the state
  • Key components:
    • App calls useReducer and passes the dispatch function into TodosDispatch.Provider
    • InputTask user can enter task name and calls the addAction on submit
    • TaskList reads the todoList state and renders a numbered task list
    • Action is a generic button component which has access to dispatch to trigger done, undo and delete actions.

Explore the CodeSandbox useReducer and useContext React todo app below to see how it’s all connected.

See my new post refactoring the above CodeSandbox why and how to use React context.

Hope this helps you tackle managing state in the next React feature you build.

If you like this post please share on Twitter.