Handling Global State with Custom Hooks

Loading

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 and theme).
  • GlobalStateProvider: This component wraps the application and provides the state and actions (setUser and setTheme) 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 using useGlobalState 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.

Leave a Reply

Your email address will not be published. Required fields are marked *