Managing global state in React can become challenging as your application grows. While React’s Context API and state management libraries like Redux can handle global state, using custom hooks for state management provides a simpler and more flexible approach to handle shared state across your app. Custom hooks allow you to encapsulate logic and make it reusable while avoiding unnecessary prop drilling.
In this guide, we will explore how to handle global state in a React application using custom hooks, and discuss how to build a simple global state management system without external libraries like Redux.
1. Why Use Custom Hooks for Global State?
Custom hooks provide an elegant way to encapsulate shared logic and state management in a reusable, modular way. By using hooks to manage global state:
- Encapsulation: Encapsulates the state logic and keeps the component clean.
- Reusability: Makes it easier to reuse the same logic across multiple components.
- Simplicity: Allows you to manage the state without relying on external state management libraries.
Custom hooks also let you combine the flexibility of React’s Context API with custom logic for state updates, and they work seamlessly with React’s useState
, useReducer
, and other hooks.
2. Using React’s Context API and Custom Hooks
To manage global state, we can use React’s Context API combined with custom hooks. The Context API provides a way to pass data through the component tree without manually passing props at every level, while custom hooks allow us to encapsulate and reuse logic for accessing and updating that state.
3. Steps to Handle Global State with Custom Hooks
We will walk through building a simple global state management solution using Context API and custom hooks.
3.1. Creating the Context for Global State
We start by creating a context that will hold the global state and allow components to consume it.
import React, { createContext, useState, useContext } from 'react';
// Create a context
const GlobalStateContext = createContext();
// Create a provider component to wrap your app with
export const GlobalStateProvider = ({ children }) => {
const [state, setState] = useState({
user: null,
theme: 'light',
});
const setUser = (user) => {
setState((prevState) => ({ ...prevState, user }));
};
const setTheme = (theme) => {
setState((prevState) => ({ ...prevState, theme }));
};
return (
<GlobalStateContext.Provider value={{ state, setUser, setTheme }}>
{children}
</GlobalStateContext.Provider>
);
};
// Custom hook to access the global state
export const useGlobalState = () => {
return useContext(GlobalStateContext);
};
3.2. Explanation of the Code
GlobalStateContext
: We create the context to hold our global state (user
andtheme
).GlobalStateProvider
: This component wraps the application and provides the state and actions (setUser
andsetTheme
) to the rest of the app via the Context API.useGlobalState
: This is a custom hook that allows other components to easily access and update the global state.
3.3. Using the Global State in Components
Now that we have set up the context and custom hook, we can use useGlobalState
in any component to read and update the global state.
Example 1: Displaying the User and Theme
import React from 'react';
import { useGlobalState } from './GlobalState';
const UserProfile = () => {
const { state } = useGlobalState();
return (
<div>
<h3>User: {state.user ? state.user.name : 'Guest'}</h3>
<p>Current Theme: {state.theme}</p>
</div>
);
};
export default UserProfile;
Example 2: Updating the Global State
import React from 'react';
import { useGlobalState } from './GlobalState';
const UpdateProfile = () => {
const { setUser, setTheme } = useGlobalState();
const handleLogin = () => {
setUser({ name: 'John Doe' });
};
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<div>
<button onClick={handleLogin}>Log in</button>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
};
export default UpdateProfile;
In this example:
UserProfile
reads the global state usinguseGlobalState
to display the current user and theme.UpdateProfile
provides buttons to update the global state (log in the user and toggle the theme).
3.4. Wrapping the Application with the Provider
Finally, we need to wrap our application in the GlobalStateProvider
to make the global state available throughout the component tree.
import React from 'react';
import ReactDOM from 'react-dom';
import { GlobalStateProvider } from './GlobalState';
import UserProfile from './UserProfile';
import UpdateProfile from './UpdateProfile';
const App = () => (
<GlobalStateProvider>
<UserProfile />
<UpdateProfile />
</GlobalStateProvider>
);
ReactDOM.render(<App />, document.getElementById('root'));
4. Advanced Features for Global State with Custom Hooks
4.1. Using useReducer
for More Complex State
For larger applications with more complex state management, you might want to replace useState
with useReducer
. This allows for more granular control over how state is updated.
Example of using useReducer
:
import { createContext, useReducer, useContext } from 'react';
const initialState = {
user: null,
theme: 'light',
};
const globalStateReducer = (state, action) => {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_THEME':
return { ...state, theme: action.payload };
default:
return state;
}
};
const GlobalStateContext = createContext();
export const GlobalStateProvider = ({ children }) => {
const [state, dispatch] = useReducer(globalStateReducer, initialState);
const setUser = (user) => {
dispatch({ type: 'SET_USER', payload: user });
};
const setTheme = (theme) => {
dispatch({ type: 'SET_THEME', payload: theme });
};
return (
<GlobalStateContext.Provider value={{ state, setUser, setTheme }}>
{children}
</GlobalStateContext.Provider>
);
};
export const useGlobalState = () => {
return useContext(GlobalStateContext);
};
4.2. Persisting Global State
For persistence, you can enhance your custom hook by saving and loading global state from localStorage or sessionStorage. This ensures that the state persists even after a page reload.
Example of persisting state with localStorage
:
const loadState = () => {
try {
const serializedState = localStorage.getItem('globalState');
if (serializedState) {
return JSON.parse(serializedState);
}
} catch (e) {
console.error('Could not load state', e);
}
return initialState;
};
const saveState = (state) => {
try {
const serializedState = JSON.stringify(state);
localStorage.setItem('globalState', serializedState);
} catch (e) {
console.error('Could not save state', e);
}
};
You can integrate this with your useReducer
or useState
logic to load and persist the global state.