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.
Understanding State and State Management.
Based on the prerequisite stated above, you likely already have some knowledge on react. But let’s refresh our memory a little bit.
What is State?
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.
State Management in React.
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.
Different State Management Options in React.
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 built-in hooks
Context API
Third-party libraries
React built-in hooks for state management
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.
useState Hook
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:
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 Initial count starts at 0
The set count function is called whenever the button is clicked, updating the count variable with the most current value at all times.
When to Use the useState Hook
The useState hook is ideal for managing state in your components when:
Local State Management: The state is only needed within a single component and does not need to be shared across multiple components.
Simple State Logic: The state logic is straightforward, such as toggling values, counters, form inputs, and simple conditionals.
Limited Component Hierarchy: The state does not need to be passed deeply through multiple layers of components, which can lead to prop drilling.
Small Sized Projects: The application does not have extensive state management needs that require more advanced solutions.
Examples:
Managing form input values.
Toggling UI elements (e.g., show/hide).
Simple counters and trackers.
The useState hook provides a simple and efficient way to handle state for these scenarios, ensuring your components remain manageable and easy to understand.
useReducer Hook
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:
reducer: This is the function where all the state updating code is executed. It takes the current state and an action as arguments and returns a new state.
initialArg: This is the initial state that you declare from the onset.
dispatch: This is the function called in the event handler. It is returned from the useReducer hook and is used to send actions to the reducer.
state: This is the current state value also returned by the useReducer hook.
Here’s a code example of how to use this hook:
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:
Just like every other hook, useReducer must be called at the top level of the component.
Every time the dispatch function is called, it triggers the reducer, leading to a state update depending on the action. This causes the component to re-render, maintaining the goal of keeping the UI and the current state in sync.
You should only specify the action type in the dispatch function and a payload if need be.
When to Use the useReducer Hook
The useReducer hook is ideal for managing state in your components when:
Complex State Logic: The state logic is complex, involves multiple sub-values, or the next state depends on the previous state.
State Transition Management: When you need to handle multiple state transitions based on different actions.
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.
State Management with Context API
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.
How to Use the Context API
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:
// 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:
Creating the Context: createContext() is used to create the context object, which includes a Provider and a Consumer.
Providing the Context: The Provider component is used to pass the current context value to the tree of components that need it.
Consuming the Context: The useContext hook is used within a functional component to access the context value.
When to Use the Context API
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:
Example: Managing themes (light or dark mode) across an entire application.
Details: The theme state is shared across multiple components, ensuring that the UI consistently reflects the selected theme.
User Authentication:
Example: Managing user authentication state and user information.
Details: The authentication state (logged in/out) and user data (username, roles, etc.) need to be accessed by various components, such as headers, profile pages, and protected routes.
Language Localization:
Example: Handling multi-language support in an application.
Details: The selected language state and translation data need to be available to text-rendering components throughout the app.
Complex State Management for Forms:
Example: Sharing form data and validation status across multiple form fields and components.
Details: Forms that span multiple steps or components can benefit from a shared state that tracks input values and validation errors.
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 Libraries for State Management
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!
State Management with 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.
How to Use Redux in React
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:
Store: The store is the central repository for an application’s state. It holds the entire state tree and provides methods to access and update the state.
Reducers: Reducers are pure functions that determine how the state changes in response to actions. They take the current state and an action as arguments and return a new state.
Actions: Actions are plain JavaScript objects that describe what happened in the application. Each action has a type property and may include additional data.
Action Creators: Action creators are functions that create and return action objects. They encapsulate the action creation logic, making the code more manageable.
Dispatch: Dispatch is a function provided by the Redux store that sends actions to the store. It triggers reducers to process the action and update the state.
Understanding these concepts is essential to effectively implementing Redux in your React application.
How to Integrate and Use Redux in Your React Project
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.
How to Use Redux Toolkit in React
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.
How to integrate and use Redux Toolkit in your React project
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.
When to Use Redux
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:
When your application has complex state logic that is difficult to manage with React’s built-in hooks like useState and useReducer.
Example: An e-commerce application with multiple product filters, user authentication, and a shopping cart.
Global State Management:
When you have state that needs to be accessed and updated by many components across different parts of your application.
Example: A user authentication system where user data needs to be accessible throughout the application.
Consistent and Predictable State:
When you need a predictable state container that helps you debug and test your application more easily.
Example: A large-scale application where you need to maintain and track the state transitions clearly.
DevTools Integration:
When you want to leverage powerful developer tools like Redux DevTools for tracking state changes and debugging.
Example: During development, Redux DevTools can help in understanding how the state changes in response to actions.
Conclusion
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.
Further reading and learning