State management is crucial in React applications because it helps keep track of application data. Since the User Interface(UI) is a function of the state, ensuring that your application's state is always up-to-date is essential. In this article, you'll learn how to choose the right state management tool to fit your application requirements.
Note: This article is for developers who already have some knowledge on react but want to make better choices for their react applications based on state management. If you’re yet to know react, check out the docs to start learning.
Based on the prerequisite stated above, you likely already have some knowledge on react. But let’s refresh our memory a little bit.
State in React is the memory of a component, containing information specific to that component. In programming terms, state is a JavaScript object that simply contains data concerning a component.
As mentioned earlier, the UI in React is directly influenced by state. Changes in state primarily occur due to user interactions such as button clicks, mouse events, input actions, and more. Therefore, managing state in our applications is essential to ensure that users experience the most current interface on their screen based on their interactions.
When a React component's state changes, it causes the component to re-render. In this process, the component is destroyed and rebuilt from scratch behind the scenes.
Most React applications experience numerous state updates as users interact with the app. It is important to use the best state management technique to enhance user experience; after all, using an unresponsive app isn't enticing. Imagine clicking the like button on your instagram app and it doesn’t respond. Annoying right?
Without further Ado let’s dive into the different state management options that you can explore for your project, explaining when and why you need each one.
There are many state management options available, but in this article, we will cover some of the most commonly used ones that cater to applications of all sizes, from small to extremely large. The options we will be discussing include:
React provides built-in hooks for managing state with functional components. These hooks are easy to use and are perfect for local state management.
Local state is the state that is only needed by one component and doesn’t affect any other component.
Global state is the state that is needed by multiple components, and we will also cover how to manage it later in this article.
Naturally, functional components are stateless, but React introduced the useState hook to enable developers to add state variables to components that need them.
This hook is called at the top level of your component with an initial state value passed in, and it returns an array of the current value and a setter function. Here’s a code example of how you can use it:
import { useState} from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
Explanation
The useState hook is ideal for managing state in your components when:
Examples:
The useState hook provides a simple and efficient way to handle state for these scenarios, ensuring your components remain manageable and easy to understand.
The useReducer hook was introduced by the React team to handle complex state logic or case-sensitive updates. Here are the key parameters you need to keep in mind while using useReducer:
Here’s a code example of how to use this hook:
import React, { useReducer } from 'react'; const initialState = { count: 0 }; function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: 'increment' })}> + </button> <button onClick={() => dispatch({ type: 'decrement' })}> - </button> </div> ); }
Key Takeaways:
The useReducer hook is ideal for managing state in your components when:
Examples of Projects that Require useReducer
Complex forms: A multi-step form in a registration process.Each step of the form collects different data, and the state needs to be managed for all steps, with validation and submission logic.
Advanced to-do-list: A to-do list application with features like adding, removing, editing, and filtering tasks.
E-commerce cart management: An e-commerce site with a shopping cart that handles adding, removing, and updating item quantities.
The previously discussed options are great, but they come with a downside: the problem of prop drilling. Prop drilling occurs when a state needs to be passed down through multiple nested components from a parent to a child. This can lead to verbose and hard-to-maintain code, as each intermediary component needs to explicitly pass the state or function down the tree.Global state, which is the state needed by multiple components, becomes particularly challenging to manage with prop drilling.
To solve this problem, React introduced the Context API, which is used for managing global state. The Context API allows you to create a context object that can be accessed by any component within its provider, eliminating the need to pass props through intermediate components.
Here’s a step-by-step guide on how to use it:
Create a Context: First, create a context using the createContext function. This creates an object with a Provider and a Consumer.
import React, { createContext } from 'react'; const MyContext = createContext();
Provide Context Value: Wrap the components that need access to the context with the Provider component. Pass the value you want to share as a prop to the Provider.
function App() { const [state, setState] = useState("Hello, World!"); return ( <MyContext.Provider value={{ state, setState }}> <ChildComponent /> </MyContext.Provider> ); }
Consume Context Value: This Use the context value in the child components by using the useContext hook or the Consumer component.
import React, { useContext } from 'react'; import MyContext from './path-to-context'; function ChildComponent() { const { state, setState } = useContext(MyContext); return ( <div> <p>{state}</p> <button onClick={() => setState("Context API is awesome!")}> Change Text </button> </div> ); }
Example Usage
Here’s a complete example demonstrating how to use the Context API:
import React, { createContext, useState, useContext } from 'react'; // Create a context const MyContext = createContext(); function App() { const [state, setState] = useState("Hello, World!"); return ( <MyContext.Provider value={{ state, setState }}> <ChildComponent /> </MyContext.Provider> ); } function ChildComponent() { const { state, setState } = useContext(MyContext); return ( <div> <p>{state}</p> <button onClick={() => setState("Context API is awesome!")}> Change Text </button> </div> ); } export default App;
Key Takeaways:
The Context API is ideal for scenarios where you need to share state or data across multiple components without having to pass props through every level of the component tree. It is particularly useful when dealing with global state or when state needs to be accessed by deeply nested components. Here are some specific cases where the Context API is beneficial:
Theming:
User Authentication:
Language Localization:
Complex State Management for Forms:
By understanding when and how to use the Context API, you can manage global state in a React application more efficiently. This approach helps avoid the pitfalls of prop drilling, keeps your codebase clean and maintainable, and contributes to creating more robust and scalable React applications.
Third-party state management libraries provide additional tools and patterns for managing state efficiently, especially in complex applications. These libraries often come with advanced features and optimizations that enhance the built-in state management solutions provided by React. Some of the most popular third-party state management libraries include Redux, MobX, Recoil, and Zustand.
In this article, we are going to cover Redux. If you need to use others mentioned, you can check out their documentation; I'll add links at the end of this article. Don’t feel overwhelmed, most of these tools are quite beginner-friendly. Now, let’s hop right into Redux!
Redux is a third-party state management library that provides an optimal solution for prop drilling and global state management by storing all state in a central place called the store. This means that all components can access this state independently, no matter their position in the component tree.
This is a game changer because as applications grow larger and there is more state to handle, it's essential to abstract it in one place. This organization makes our code cleaner and debugging easier. Sounds great, right?
Keep in mind that Redux isn't specifically restricted to React; it is an independent library that can be integrated with other JavaScript frameworks like Angular, Vue, and more.
Before we go into the step-by-step process of using Redux, it's important to understand the key concepts that form the foundation of Redux:
Understanding these concepts is essential to effectively implementing Redux in your React application.
In this subsection, you will learn a step-by-step approach to integrating Redux with your React projects. We'll use a simple counter-example to illustrate the process. Here are the steps:
Setting up your Project
Create a React app with Vite:
npm create vite@latest projectName
Navigate into your project directory:
cd projectName
Install Redux Toolkit and React-Redux:
npm install @reduxjs/toolkit react-redux
Creating the Redux Store: Create a new file src/app/store.js and set up the Redux store:
import { createStore } from 'redux'; import rootReducer from '../features/counter/counterReducer'; const store = createStore(rootReducer); export default store;
Creating the Reducer: Create a new directory src/features/counter and inside it, create a file counterReducer.js:
const initialState = { value: 0, }; function counterReducer(state = initialState, action) { switch (action.type) { case 'INCREMENT': return { ...state, value: state.value + 1 }; case 'DECREMENT': return { ...state, value: state.value - 1 }; case 'INCREMENT_BY_AMOUNT': return { ...state, value: state.value + action.payload }; default: return state; } } export default counterReducer;
Creating Actions: In the same directory, create a file counterActions.js:
export const increment = () => ({ type: 'INCREMENT', }); export const decrement = () => ({ type: 'DECREMENT', }); export const incrementByAmount = (amount) => ({ type: 'INCREMENT_BY_AMOUNT', payload: amount, });
Providing the Store to Your App: Wrap your application with the Redux Provider in src/main.jsx:
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import store from './app/store'; import App from './App'; import './index.css'; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
Connecting React Components to Redux: In your src/App.jsx, use the Redux state and dispatch actions:
import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { increment, decrement, incrementByAmount } from './features/counter/counterActions'; function App() { const count = useSelector((state) => state.value); const dispatch = useDispatch(); return ( <div> <p>Count: {count}</p> <button onClick={() => dispatch(increment())}>+</button> <button onClick={() => dispatch(decrement())}>-</button> <button onClick={() => dispatch(incrementByAmount(2))}>+2</button> </div> ); } export default App;
This is how to use Redux in your React applications. If you need to know more, you can check the documentation. However, Redux has introduced a more optimized way of writing Redux applications with Redux Toolkit (RTK).
Before RTK, the legacy Redux was the only way to use Redux. Now, we have Redux Toolkit with some optimized features, and that is what we will be covering in the next section.
RTK introduces several key concepts that simplify state management. The major ones you need to know are:
Slices: A slice is a collection of Redux reducer logic and actions for a single feature of your application. It streamlines the process of writing reducers and actions into a single unit.
createSlice: This RTK function helps you create a slice, automatically generating action creators and action types. It reduces boilerplate code significantly.
configureStore: This function simplifies the process of creating a Redux store by providing good defaults, including integration with the Redux DevTools Extension and middleware like redux-thunk.
createAsyncThunk: This function is used for handling asynchronous logic. It generates actions and action creators to manage different stages of an asynchronous operation (e.g., pending, fulfilled, and rejected).
Selectors: Functions that extract and derive pieces of state from the store. RTK encourages using selectors to encapsulate and reuse state logic.
RTK Query: An advanced data fetching and caching tool built into RTK. It simplifies handling server-side data, reducing the need for boilerplate code related to data fetching, caching, and synchronization.
Understanding these concepts is essential for effectively implementing Redux Toolkit in your React application.
In this subsection, you'll learn a step-by-step approach to integrating Redux Toolkit with your React projects. We’ll use a simple counter example, similar to the one used in the plain Redux example, to highlight the improvements and optimizations Redux Toolkit offers. Here are the steps:
Setting up your Project
Create a React app with Vite:
npm create vite@latest projectName
Navigate into your project directory:
cd projectName
Install Redux Toolkit and React-Redux:
npm install @reduxjs/toolkit react-redux
Creating a Redux Slice: Create a new file for your slice (e.g., counterSlice.js):
import { createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { count: 0 }, reducers: { increment: (state) => { state.count += 1; }, decrement: (state) => { state.count -= 1; }, }, }); export const { increment, decrement } = counterSlice.actions; export default counterSlice.reducer;
Configuring the Store: Create a new file for your store (e.g., store.js):
import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './counterSlice'; const store = configureStore({ reducer: { counter: counterReducer, }, }); export default store;
Providing the Store to Your App: Wrap your app with the Provider component in your main file (e.g., main.js or index.js):
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import store from './store'; import App from './App'; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
Using Redux State and Actions in Your Components: Use the useSelector and useDispatch hooks in your component (e.g., Counter.js):
import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { increment, decrement } from './counterSlice'; function Counter() { const count = useSelector((state) => state.counter.count); const dispatch = useDispatch(); return ( <div> <p>{count}</p> <button onClick={() => dispatch(increment())}>+</button> <button onClick={() => dispatch(decrement())}>-</button> </div> ); } export default Counter;
Redux Toolkit (RTK) simplifies and optimizes the traditional Redux setup by reducing boilerplate code and integrating essential tools and best practices. While legacy Redux requires manual configuration and verbose code for actions and reducers, RTK offers a more streamlined approach with utility functions like configureStore, createSlice, and createAsyncThunk.
RTK includes built-in middleware, integrates seamlessly with Redux DevTools, and promotes a standard way of writing Redux logic, making state management in React applications more efficient and maintainable. If you need to use Redux, I recommend using the modern Redux Toolkit, as it is now recommended by Redux. You can check the docs to learn more about RTK.
Redux is a powerful state management library, but it isn't always necessary for every React application. Here are some scenarios when using Redux might be beneficial:
Complex State Logic:
Global State Management:
Consistent and Predictable State:
DevTools Integration:
I hope by now you have gained more clarity and insights into choosing the right state management tool for your projects. We have covered tools that cater to both small and extremely large projects. With the knowledge gained from this article, you can now make more informed decisions for your projects. See you next time on another insightful topic.
Redux docs
Zustand docs
Mobx docs
Recoil docs
React docs
The above is the detailed content of State Management in Reactjs: A Guide to Choosing the Right State Management Tool for Your Projects. For more information, please follow other related articles on the PHP Chinese website!