State Management in ReactJS
As a frontend engineer, you're constantly faced with the challenge of managing application state effectively. The state of your application includes various data elements that are crucial for its proper functioning. In this blog post, we will explore the concept of state, dive into the realm of state management, and provide you with a comprehensive list of recommended state management solutions. Finally, we will discuss essential factors to consider when choosing the right state management approach for your frontend projects.
State Management
Before we delve into state management, let's start by defining what state means in the context of web development. In simple terms, state represents the current condition or data of your application at a given moment. It includes variables, objects, or any other data structures that hold information necessary for your application's behavior and user interactions.
State management refers to the process of handling and manipulating application state in a consistent and efficient manner. It involves techniques and tools that allow you to control how state is accessed, modified, and shared across various components or modules of your frontend application.
Things like, "Whether modal is open or closed" and "message content in toolbox" are piece of state. In project, mau be you name it isModal
where the app set to open
or close
and messageToolbox
with string message.
We won't discuss when or why we need to use State Management in this article, as it requires a separate blog post. However, the simple reason for using state management is to avoid props drilling.
Recommendation for State Management
There are several popular state management solutions available for frontend engineers. Here's a list of some widely used options:
React Context API:
React Context API is built-in React that provide simple way to manage state. This is alternative to How to cope "prop drilling" in simple way. React Context often to use for anage state for UI. Things like Modal, Sidebar, Tooltips or something like that.
Example
Step 1: Creating and Exporting the Context Create a context for your data. Place this code outside of any components, typically at the top level of a file, and export it for later use.
export const MyDataContext = React.createContext();
Step 2: Providing Data in the Component In the component where you want to provide the data, import the context and use the Provider
component to pass the data down to the child components.
function TheComponentWithState() {
const [state, setState] = useState('whatever');
return (
<MyDataContext.Provider value={state}>
{/* Component's content goes here */}
<ComponentThatNeedsData />
</MyDataContext.Provider>
);
}
Step 3: Accessing Data in a Subtree Within any component located under the Provider
in the component tree, you can access the data by using the useContext
hook.
function ComponentThatNeedsData() {
const data = useContext(MyDataContext);
// Use the data here
}
By utilizing this approach, you can create a context, provide data using the Provider
, and access that data within any component under the Provider
using the useContext
hook.
Redux
Redux is a powerful and widely adopted state management library for JavaScript applications. It follows a predictable state container pattern, allowing you to centralize and manage complex application states efficiently.
You create global store with immutability that hold all of app state. There will be reducer function that receive action that you dispatch from your components,and respond by returning a new copy of state.
But as one of Redux Maintenancer, we recommendation to use Redux Toolkit for simplify boilerplate code.
Example
Step 1: Setting Up the Redux Store Create a Redux store using Redux Toolkit. This involves defining a slice with the initial state and a reducer function.
import { createSlice, configureStore } from '@reduxjs/toolkit';
const myDataSlice = createSlice({
name: 'myData',
initialState: 'whatever',
reducers: {
updateData: (state, action) => {
return action.payload;
},
},
});
const store = configureStore({
reducer: {
myData: myDataSlice.reducer,
},
});
Step 2: Providing Data in the Component In the component where you want to provide the data, import the necessary Redux Toolkit functions. Use the useDispatch
hook to dispatch actions to update the data.
import { useDispatch } from 'react-redux';
import { updateData } from './myDataSlice';
function TheComponentWithState() {
const dispatch = useDispatch();
const handleDataUpdate = () => {
dispatch(updateData('new data'));
};
return (
<>
{/* Component's content goes here */}
<button onClick={handleDataUpdate}>Update Data</button>
<ComponentThatNeedsData />
</>
);
}
Step 3: Accessing Data in a Subtree Within any component located under the Redux Provider
, you can use the useSelector
hook to access the data from the Redux store.
import { useSelector } from 'react-redux';
function ComponentThatNeedsData() {
const data = useSelector((state) => state.myData);
// Use the data here
return <div>{data}</div>;
}
By utilizing Redux Toolkit, you can create a Redux store, dispatch actions to update the data, and access that data within any component using the useSelector
hook.
Zustand
Zustand, a lightweight state management library tailored for React, stands out from React's context API. One advantage is that it eliminates the need for a provider, reducing the chances of encountering errors. Additionally, Zustand minimizes boilerplate code commonly found in other state management systems. It embraces the use of hooks as the primary pattern for interacting with the state, providing a simpler and more intuitive approach.
Example
- Import the
create
function from Zustand in your component:
import create from 'zustand';
2. Use the create
function to create a store with an initial state and functions to update the state:
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
In this example, we create a store with an initial state of { count: 0 }
and two functions, increment
and decrement
, to update the count.
3.Use the useStore
hook to access the store in your component:
function Counter() {
const { count, increment, decrement } = useStore();
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
In this example, we use the useStore
hook to access the store in our Counter
component. The count
, increment
, and decrement
values returned by the hook can be used to display the current count and update it when the buttons are clicked.
Recoil
Recoil is a state management library specifically designed for React applications. Recoil lets you create a data-flow graph that flows from atoms (shared state) through selectors (pure functions) and down into your React components. Atoms are units of state that components can subscribe to. Selectors transform this state either synchronously or asynchronously.
Recoil is similar to Zustand in that it is tailored for React. It is React-centric, with state changes flowing from the roots of the graph (atoms) through pure functions (selectors) and into components.
Example
Open the index.js
file and create a Recoil store.
import { RecoilRoot } from 'recoil';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<RecoilRoot>
<App />
</RecoilRoot>,
document.getElementById('root')
);
Create a new ReactJS component and use Recoil.
import React from 'react';
import { useRecoilState } from 'recoil';
import { countState } from './store';
function Counter() {
const [count, setCount] = useRecoilState(countState);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase</button>
<button onClick={() => setCount(count - 1)}>Decrease</button>
</div>
);
}
export default Counter;
Finally, import the Counter
component into your main App
component and render it.
import React from 'react';
import Counter from './Counter';
function App() {
return (
<div>
<Counter />
</div>
);
}
export default App;
Choosing State Management
When selecting a state management solution for your frontend project, consider the following factors:
a. Project Size and Complexity: Assess the size and complexity of your application. Smaller projects with simple state requirements might benefit from lightweight solutions like React Context API or Zustand, while larger projects may require more robust options like Redux Toolkit
b. Learning Curve: Evaluate the learning curve associated with each state management solution. Consider the familiarity of your team with a particular library and its documentation, as it will impact development time and maintenance.
c. Ecosystem and Community Support: Take into account the availability of resources, third-party integrations, and community support for the state management library. A vibrant ecosystem ensures easy access to tutorials, libraries, and best practices.
d. Developer Experience: Consider how well the state management solution integrates with your existing development tools, IDEs, and debugging utilities. A seamless developer experience can greatly improve productivity and ease of maintenance.