javascriptreactjsreact-context

How can I use React Context API efficiently


I'm making React web application and beginner. As I used Context API, I had a problem.

Here's my code.

import React, { createContext, useState, useEffect } from "react";

export const RoleContext = createContext({
  optionRole: "",
  setOptionRole: () => {},
  accessToken: "",
  setAccessToken: () => {},
  passwordChecked: false,
  setPasswordChecked: () => {},
});

export const RoleProvider = ({ children }) => {
  const [optionRole, setOptionRole] = useState(() => {
    return localStorage.getItem("optionRole") || "";
  });
  const [accessToken, setAccessToken] = useState("");
  const [passwordChecked, setPasswordChecked] = useState(false);

  useEffect(() => {
    
    const refreshAccessToken = async () => {
      try {
        const response = await fetch("/auth/token", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${accessToken}`,
          },
        });

        if (!response.ok) {
          throw new Error("Failed to refresh access token");
        }

        const data = await response.json();
        setAccessToken(data.accessToken);
      } catch (error) {
        
        console.error("Failed to refresh access token:", error);
        
        // logout();
      }
    };

        if (!accessToken) {
      refreshAccessToken();
    }
  }, [accessToken]);

  useEffect(() => {
        localStorage.setItem("optionRole", optionRole);
  }, [optionRole]); 

  const roleCtx = {
    optionRole,
    setOptionRole,
    accessToken,
    setAccessToken,
    passwordChecked,
    setPasswordChecked,
  };

  return (
    <RoleContext.Provider value={roleCtx}>{children}</RoleContext.Provider>
  );
};

The context name is RoleContext. It means that the component will deal with role. But I want another Context API. If I make another Context API. The code would be messy. For example, I want to make MathContext. I may use it this way. And supposing that I want to Context API more, the more I want to make, the more the code is complicate.

<SubjectProvider>
 <MathProvider>
  <RoleProvider>
   <App/>
  </RoleProvider>
 <MathProvider>
</SubjectProvider>

Is there any good way to use Context API?


Solution

  • If you are worrying about the problem that you have many Context Providers so you can avoid it by moving your logic to the reducers (useReducer) and use the only one Context Provider to share your reducer's state and it's dispatch action down to the components tree.


    The good practice regarding your question is to create a multiple reducers for each of your logic unit/purpose and thanks to this approach you will follow Single Responsibility pattern and it will be easier to maintain these reducers.

    Then you cam combine your reducers into single one for the centralized source of truth and use this rootReducer in the context to make the state and dispatch accessible for the children.
    In this case you will have only 1 Context provider but it might contain multiple reducers and every reducer will be responsible for it's own purpose so you will get your functionality without many context providers.

    Here is the code example:


    counter.reducer

    export const initialCounterState = { count: 0 };
    
    export function counterReducer(state = initialCounterState, action) {
      switch (action.type) {
        case "INCREMENT":
          return { count: state.count + 1 };
        case "DECREMENT":
          return { count: state.count - 1 };
        default:
          return state;
      }
    }
    

    theme.reducer

    export const initialThemeState = { theme: "light" };
    
    export function themeReducer(state = initialThemeState, action) {
      switch (action.type) {
        case "TOGGLE_THEME":
          return { theme: state.theme === "light" ? "dark" : "light" };
        default:
          return state;
      }
    }
    

    root.reducer where multiple reducers are combined together

    import { counterReducer, initialCounterState } from "./counter.reducer";
    import { themeReducer, initialThemeState } from "./theme.reducer";
    
    export function rootReducer(state, action) {
      return {
        counter: counterReducer(state.counter, action),
        theme: themeReducer(state.theme, action)
      };
    }
    
    export const initialRootState = {
      counter: initialCounterState,
      theme: initialThemeState
    };
    

    AppContext that will use your root Reducer to make it accessible for the children:

    import React, { createContext, useReducer, useContext } from "react";
    import { rootReducer, initialRootState } from "./reducers/index";
    
    const AppCtx = createContext();
    
    export function AppProvider({ children }) {
      const [state, dispatch] = useReducer(rootReducer, initialRootState);
    
      return (
        <AppCtx.Provider value={{ state, dispatch }}>{children}</AppCtx.Provider>
      );
    }
    
    export function useAppContext() {
      const ctx = useContext(AppCtx);
      if (!ctx) {
        throw new Error("useAppContext must be used within a AppProvider");
      }
      return ctx;
    }
    

    Components can use it in this way thanks to the hook in the AppContext:
    import React from "react";
    import { useAppContext } from "./AppContext";
    
    const Counter = () => {
      const { state, dispatch } = useAppContext();
      const { count } = state.counter;
    
      return (
        <div>
          <h1>Count: {count}</h1>
          <button onClick={() => dispatch({ type: "INCREMENT" })}>Increment</button>
          <button onClick={() => dispatch({ type: "DECREMENT" })}>Decrement</button>
        </div>
      );
    };
    
    export default Counter;
    

    ThemeSwitcher

    import React from "react";
    import { useAppContext } from "./AppContext";
    
    const ThemeSwitcher = () => {
      const { state, dispatch } = useAppContext();
      const { theme } = state.theme;
    
      return (
        <div>
          <h1>Current Theme: {theme}</h1>
          <button onClick={() => dispatch({ type: "TOGGLE_THEME" })}>
            Toggle Theme
          </button>
        </div>
      );
    };
    
    export default ThemeSwitcher;
    



    And don't forget to wrap your child components into the Context Provider:

    import React from "react";
    
    import { AppProvider } from "./AppContext";
    import Counter from "./Counter";
    import ThemeSwitcher from "./ThemeSwitcher";
    
    const App = () => {
      return (
          <AppProvider>
            <Counter />
            <ThemeSwitcher />
          </AppProvider>
      );
    };
    
    export default App;
    

    Here is the live example I made for this purpose.

    sandbox example