If you havenât used state management excessively in React Function Components , this tutorial may help you to get a better understanding of how React Hooks â such as useState, useReducer, and useContext â can be used in combination for impressive state management in React applications. In this tutorial, we will almost reach the point where these hooks mimic sophisticated state management libraries like Redux for globally managed state. Letâs dive into the application which we will implement together step by step.
We start with a list of items â in our scenario a list of todo items â which are rendered in our function component with a JavaScript Map Method for Arrays . Each todo item rendered as list item receives a key attribute to notify React about its place in the rendered list :
In order to add a new todo item to our list of todo items, we need an input field to give a new todo item a potential task property. The id and complete properties will be automatically added to the item. In React, we can use the State Hook called useState to manage something like the value of an input field as state within the component:
We also had to give our Function Arrow Component a body with an explicit return statement to get the useState hook in between. Now, we can change the task state with our handler function, because we have the inputâs value at our disposal in Reactâs synthetic event:
Now the input field has become a controlled input field , because the value comes directly from the React managed state and the handler changes the state. We implemented our first managed state with the State Hook in React. The whole source code can be seen here .
To continue, letâs implement a submit button to add the new todo item to the list of items eventually:
The submit handler doesnât add the new todo item yet, but it makes the input fieldâs value empty again after submitting the new todo item. Also it prevents the default behavior of the browser, because otherwise the browser would perform a refresh after the submit button has been clicked .
In order to add the todo item to our list of todo items, we need to make the todo items managed as state within the component as well. We can use again the useState hook:
By having the setTodos function at our disposal, we can add the new todo item to the list. The built-in array concat method can be used for this kind of scenario. Also the shorthand property name is used to allocate the task property in the object:
There is one flaw in this implementation. The new todo item has always the same identifier, which shouldnât be this way for a unique identifier. Thatâs why we can use a library to generate a unique identifier for us. First, you can install it on the command line:
Second, you can use it to generate a unique identifier:
You have implemented your second use case for managing state in React by appending an item to a list of items. Again it was possible with the useState hook. The whole source code can be seen here and all changes here .
Last but not least, letâs implement a checkbox for each todo item in the list to toggle their complete flags from false to true or true to false.
Since we need the id of the todo item in our handler function, and not the event, we use a wrapping arrow function to pass the identifier of the individual todo item to our handler:
Last, by having the id at our disposal, we can only alter the affected todo item in our list â by negating the complete flag â and return every other todo item as before. By using the map method, we return a new array made up of the changed todo item and the remaining todo items :
Thatâs it. The new todo items are immediately set as state for the list of todo items with the setTodos function. The whole source code can be seen here and all changes here . Congratulations, you have implemented a whole todo application with three use cases for state management with the useState hook:
The useState hook is great to manage simple state. However, once you run into more complex state objects or state transitions â which you want to keep maintainable and predictable â, the useReducer hook is a great candidate to manage them. Here you can find a comparison of when to use the useState or useReducer hook. Letâs continue implementing our application with the useReducer hook by going through a simpler example first. In our next scenario, we want to add buttons to filter our list of todos for three cases:
Letâs see how we can implement these with three buttons:
We will care later about these. Next, letâs see how we can map the three cases in a reducer function:
A reducer function always receives the current state and an action as arguments. Depending on the mandatory type of the action, it decides what task to perform in the switch case statement, and returns a new state based on the implementation details. In our case, the implementation details are straightforward:
Now we can use the reducer function in a useReducer hook. It takes the reducer function and an initial state and returns the filter state and the dispatch function to change it:
First, the dispatch function can be used with an action object â with an action type which is used within the reducer to evaluate the new state:
Second, â after we are able to transition from state to state with the reducer function and the action with action type â the filter state can be used to show only the matching todo items by using the built-in array filter method :
The filter buttons should work now. Every time a button is clicked an action with an action type is dispatched for the reducer function. The reducer function computes then the new state. Often the current state from the reducer functionâs argument is used to compute the new state with the incoming action. But in this simpler example, we only transition from one JavaScript string to another string as state.
The whole source code can be seen here and all changes here .
Note: The shown use case â also every other use case with useReducer â can be implemented with useState as well. However, even though this one is a simpler example for the sake of learning about it, it shows clearly how much its helps for the reasoning for the state transitions by just reading the reducer function.
The useReducer hook is great for predictable state transitions as we have seen in the previous example. Next, we are going to see how it is a good fit for complex state objects too. Therefore, we will start to manage our todo items in a reducer hook and manipulate it with the following transitions:
The reducer would look like the following:
The following transitions are implemented in the reducer:
Instead of the useState hook from before, we can manage our todos with this new reducer and the initially given todo items:
If someone toggles a todo item with the checkbox element, a new handler is used to dispatch an action with the appropriate action type depending on the todo itemâs complete status:
If someone submits a new todo item with the button, the same handler is used but to dispatch an action with the correct action type and the name of the todo item ( task ) and its identifier ( id ) as payload:
Now everything that has been managed by useState for our todo items is managed by useReducer now. The reducer describes what happens for each state transition and how this happens by moving the implementation details in there. The whole source code can be seen here and all changes here .
You have seen how useState and useReducer can be used for simple and complex state management whereas useReducer gives you clear state transitions â thus improved predictability â and a better way to manage complex objects.
We can take our state management one step further. At the moment, the state is managed co-located to the component. Thatâs because we only have one component after all. What if we would have a deep component tree? How could we dispatch state changes from anywhere?
Letâs dive into Reactâs Context API and the useContext hook to mimic more a Reduxâs philosophy by making state changes available in the whole component tree. Before we can do this, letâs refactor our one component into a component tree. First, the App component renders all its child components and passes the necessary state and dispatch functions to them:
Second, the Filter component with its buttons and handlers which are using the dispatch function:
Third, the TodoList and TodoItem components. Since the individual TodoItem component defines its own handler, the onChange event handler doesnât need to pass the todo item anymore. The item is already available in the component itself:
Last, the AddTodo component which uses is own local state to manage the value of the input field:
In the end, we have a component tree whereas each component receives state as props and dispatch functions to alter the state. Most of the state is managed by the parent App component. Thatâs it for the refactoring. The whole source code can be seen here and all changes here .
Now, the component tree isnât very deep and it isnât difficult to pass props down. However, in larger applications it can be a burden to pass down everything several levels. Thatâs why React came up with the idea of the context container. Letâs see how we can pass the dispatch functions down with Reactâs Context API. First, we create the context:
Second, the App can use the contextâs Provider method to pass implicitly a value down the component tree:
Now, the dispatch function doesnât need to be passed down to the components anymore, because itâs available in the context:
The useContext hook helps us to retrieve the value from the context in the AddTodo component:
Also the TodoItem component makes use of it and the dispatch function doesnât need to be passed through the TodoList component anymore:
The application works again, but we are able to dispatch changes for our todo list from anywhere. If you want to continue with this application, experiment with passing down the dispatch function for the filter reducer as well. Moreover, you can pass the state coming from useReducer with Reactâs Context API down as well. Just try it yourself. The whole source code can be seen here and all changes here .
You have learned how modern state management is used in React with useState, useReducer and useContext. Whereas useState is used for simple state (e.g. input field), useReducer is a greater asset for complex objects and complicated state transitions. In larger applications, useContext can be used to pass down dispatch functions (or state) from the useReducer hook.