Advanced State Management in React
So far, in our previous post on state management, we have seen how to manage a state using useState and useReducer hooks. There is another hook known as useContext for managing a global state within a react application. Specifically, this hook is best used when you want to share data across the entire application such as authenticated user details, theme settings, etc.
Enough, why the need for useContext when we can use props to share data from component to component? Well, while props are good in sharing data, especially from a parent to a child component, it leads to a problem popularly referred to as prop drilling, that is, a situation where a prop is passed down to the component tree (passing through all intermediaries, even if they don’t need it) until it gets to the target component. And that’s where the useContext hook comes to the rescue.
Instead of passing data through all the intermediaries’ components to the target component, we simply wrap the parent component with context and then access the context from the desired component. Let’s see it in action.
Suppose we have a parent component called App with two children X and Y. Component X has also three components namely A, B, and C. Component Y has two components, D and E. If component C wants to access data that is available in the App component, the normal tradition would have been to pass the data as a prop down to component C. However, with useContext, we will achieve it as follows:
import { createContext } from “react”;
const myContext = createContext()
function App() {
return (
<myContext.Provider value={{ user: ‘ufaz’ }}>
<div>
<X />
<Y />
</div>
</myContext>
)
}
From the above example, we started by importing the createContext from react, next, we created a context (called myContext) using the imported useContext hook. We also created the App component and within the return statement, we used the created context (myContext) like an element and wrapped the children component (X and Y). What is special in the above code is in the accessing of the Provider property from the created context (myContext.Provider). We also pass a prop called value which takes data that need to be shared across the entire application. In our case, it is an object with the name of the authenticated user. To be specific, we have just provided the context which is going to be shared across the entire application. How does the children component get access to the shared data? Let’s see.
import {useContext } from “react”;
import myContext from “./MyContextFile”;
function C() {
const context = useContext(myContext)
return <div>Username: {context.user} </div>
}
From the above code, we started by importing the useContext from react and then also imported the created context, which by convention is usually created in a separate file. We then created a component called C, and within it, we used the useContext (which by default accepts the context object) and passed to it as an argument, the myContext object. We then returned the username by accessing the context (context.user). And yes, we have access to the shared or global data without the hassle of using props.
This is the basic idea of how the useContext hook is used in react. Note that the data to share can be beyond a simple object as demonstrated above. This will depend on the type of application you are managing. For information on the usage of useContext or Context API as a whole, kindly read the documentation.
Managing State with third-party Libraries
Previously, we have learned how to manage a state in react with the aid of useState, useReducer, and useContext. However, there are varieties of tools or third-party libraries that can also be of use to manage complex state in react, such as Redux, Redux Toolkit (RTK), MobX, Recoll, Zustand, GraphQL, etc. where each has its own pros and cons, and the type of the project/team will further determine the type library to use. Therefore, in this section, we will learn how to use Redux Toolkit to manage a state in a react. To be honest, RTK is my favorite. Let’s deep dive into RTK.
What is Redux Toolkit (RTK)?
As earlier mentioned, is a state management library that can be of use to manage a complex state. It was created by the react team with a set of tools for managing state instead of using pure redux, and has become the standard and recommended approach for managing state over pure redux.
RTK provides an efficient approach to writing redux code with less boilerplate code and generally makes it easier and or simpler to use toward managing state, especially, in a complex application.
How does RTK Work?
RTK has some similarities with useReducer. The key components involve a store, reducers, actions, and state. The store represents the central location for holding the states, the reducers represent where the logic of managing a state takes place, and actions represent the operations (for instance, CRUD operations), which are usually dispatched. The state is a variable referencing the various variables initialized in the initial state.
To demonstrate the use of RTK, we will create a To-do-app for managing Todos with abilities to create a to-do, mark it completed (with a checkbox), and delete a to-do. To access the complete application, check my GitHub account which can be found here.
However, this time, we will create our application with vite. Open your terminal, and navigate to where you want the application to reside. Enter the following command.
npm create vite@latest todo-app –-template react
next, navigate to the folder where you created the application (todo-app) and run the below commands.
npm install
npm run dev
Remember we mentioned that RTK is a library, which means it is not part of the built-in tools from react. Therefore, we need to install it separately. Open another terminal, navigate to your application directory, and run the below commands.
npm install @reduxjs/toolkit react-redux
If that is successful, redux toolkit and react-redux will be added to your dependencies list which is located in your package.json file.
Within your src folder, create three folders, a components folder for holding UI components, a features folder for creating slices, and a store folder for holding the store. Inside the components folder, create a Todo.jsx, a TodoList.jsx, and TodoItem.jsx respectively. In the features folder, create a todosSlice.js file. Inside the store folder, create a store.js file.
Let’s start by tackling the logic of the todo application which is inside the todosSlice.js file. First, we will import the createSlice function from the reduxjs/toolkit that we installed as below:
import { createSlice } from “@reduxjs/toolkit”;
We then initialize a variable as the initial state as below. In our example, the initial state is an object, with a todos variable set to contain an array (initially empty) of objects with id, title, and isDone.
const initialState = { todos: [] }
Next, we created the slice (named todosSlice) using the imported createSlice function. It also contains an object with a given name (in our example, we called it todos), the initialState earlier created, and the reducers (which are the actual logic for managing state) which are also an object. In our application, the user will be able to create a todo, mark it completed, and be able to delete a todo. These three things represent the actions or logic of the application as shown below:
const todosSlice = createSlice({
name: “todos”,
initialState,
reducers: {
createTodo: (state, action) => {
state.todos.push(action.payload)
},
completedTodo: (state, action) => {
state.todos = state.todos.map((todo) => {
todo.id === action.payload.id ? {...todo, isDone: !todo.isDone}
: todo
);
return state;
}
},
deleteTodo: (state, action) => {
state.todos = state.todos.filter((todo) => todo.id !== action.payload.id);
return state;
},
},
});
export const { createTodo, completedTodo, deleteTodo } = todosSlice.actions;
export default todosSlice.reducers;
From the above code and as earlier mentioned, we created our slice and named it todosSlice. The createSlice function takes an object with a name, initialState, and reducers. Reducers is also an object with functions that represent the various actions the user will perform with our application. The first function called createTodo takes two parameters, a state, and an action. The state provides us a means of accessing the variable (todos), while the action is used to access the data that will be used by the function. The createTodo adds a new todo by pushing it to the todos array. You may wonder why are we mutating a state directly, Yes, is because behind the scenes, react takes care of it.
Next, the completedTodo function also takes two parameters, state & action. The goal is to toggle the isDone property from either false to true or vice versa, which will be based on which a todo checkbox is checked or unchecked. To achieve this, we need to know the id of which todo was clicked, and then map through the todos using the id. If found, we toggle the isDone property only without changing other properties of the identified todo. In the completedTodo function, this was achieved using the map function for iterating over the todos and a ternary operator for checking the targeted todo id. Finally, in the completedTodo, we returned the state after the update of the matched todo.
As for deleting a todo, we created a function called deleteTodo that also takes the state and an action as parameters. To delete a todo, we get the id of the todo and filter out the todo from our todos whose id matched with the todo’s delete button that was clicked. We also then returned a state.
领英推荐
Outside of the createSlice function, we exported the functions created within the reducers as actions. They serve as the action creators that will dispatch when the need arises. Lastly, the todosSlice was also exported as default using the todosSlice.reducer.
Configuring the Store
Now that our logic is completed, how do other components get access to the logic? We now need to create a store (serving as the ultimate source of truth for every action) and include any reducers we are interested in. This is shown below, which is contained in the store.js file earlier created.
import { configureStore } from “@reduxjs/toolkit”;
import todosSlice from “./features/todosSlice”
const store = configureStore({
reducer: todosSlice
});
export default store;
We started by importing the configureStore from the redux toolkit. In the next line, we also imported the todosSlice created from the todosSlice.js file. We created the store variable using the configureStore function which also accepts an object as a parameter. In our case, we specified our todosSlice as the reducer. Finally, we exported the store.
Configuring the store is just the first step, next, we need to make the store available for other components to access the store. This is achieved by wrapping the top-most component of the application (which is Todo) with the store, as shown below:
// App.jsx
import { Provider } from “react-redux”;
import store from “./store/store”;
import Todo from “./components/Todo”;
function App() {
return (
<Provider store={store}>
<Todo />
</Provider?
);
}
export default App;
What happened above? We imported the Provider component from react redux, alongside the store we created earlier, and the Todo component. The point of interest is the return statement of the App component. We used the Provider component which also takes a prop called store that is assigned the imported store configured, to wrap the Todo component. With this setting, we have made the store accessible to not only the Todo component, but also, including the children components of the Todo.
Accessing and Dispatching Actions
Having configured the store and the createSlice logic, we will now discuss how we can access the state variables in the initial state and how we can dispatch actions to the reducer logic. From the react-redux, we have access to two hooks, referred to as useSelector for accessing state and useDispatch for sending actions to logic. Let’s see them in action.
To access the state variables (in our case, the todos variable), we first import the useSelector hook from react-redux as shown below:
import { useSelector } from “react-redux”;
We then create a variable that will hold the returned result of useSelector hook. The useSelector hook takes a function that returns the values of the state variables as shown below
const todos = useSelector((state) => state.todos);
With this, we can then use the map function to iterate over the collection of objects and render them appropriately as shown below:
// Todo.jsx
{
todos.length > 0 ? (
<TodoList todos={todos} />
) : (
<p>Your todo list is empty</p>
)
}
Inside the TodoList component
//TodoList.jsx
function TodoList({ todos }) {
return (
<ul>
{
todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
)
)}
</ul>
)
}
However, to dispatch action, we use the useDispatch function alongside the appropriate action reducers we earlier exported. In our application, we want to create a todo as soon as the user clicks the add button after entering the title of the todo in the input field provided. This can be achieved as follows. First, we import the useDispatch hook as below
import { useDispatch } from “react-redux”
Next, we create a dispatch variable for sending appropriate action as shown below.
const dispatch = useDispatch();
The useDispatch hook does not take any argument and must be declared before the return statement of the component. Now to handle the creation of a new todo, we will create an event handler function that when the add button is clicked will trigger the function. Within this handler function, we dispatch the creation of the todo alongside the appropriate action reducers, as shown below:
import { createTodo } from “../features/todosSlice”;
before the return state of the component, we write:
const handleAddTodo = (e) => {
e.preventDefault();
if (title === “”) return;
dispatch(createTodo({id: uuid(), title: title, isDone: false});
setTitle(“”)
}
In the above event handler function called handleAddTodo, we pass event (e) as the event variable used for preventing the default form behavior (refreshing). Next, we check if the input’s field value is an empty string, then it should not submit. The dispatch takes the imported createTodo action reducers imported and passed in an object with id (using uuid for creating id’s), the title (which takes value from the input field), and the isDone (which is by default set to false). Lastly, we reset the input field to allow for another entry.
Similarly, to handle the deletion and toggling of the isDone property, two more event handlers were created (handleDelete and handleComplete) as shown below.
const handleDelete = (id) => {
dispatch(deleteTodo({id: id})
}
const handleComplete = (id) => {
dispatch(completedTodo({id: id})
}
And that’s it about basic usage of React Toolkit. Also, note that it supports a number of features, like data fetching, and caching with the support React Toolkit Query. Interested reader can visit the official documentation for more.
Summary
In this section, we discussed a mechanism for managing a global state with the aid of useContext hook, which solves the problem of prop drilling. Next, we discussed advanced state management, with the support of a third-party library, specifically, React Toolkit, to manage states in a react application. We have seen that using RTK requires the creation of reducers (logic), actions, a store, and the initial state.