DEV Community

loading...
Cover image for React Global State with useContext

React Global State with useContext

Rufat Aliyev
I am a Canada-based professional front-end developer that has a strong passion for making things happen and loves the team aspect of software development.
Updated on ・3 min read

Imagine your global state API looks like this.

const Index: React.FC = () => {
  const { loading, recipeList, getRandomRecipes} = useStore();
...
Enter fullscreen mode Exit fullscreen mode

Just one hook that provides all you need from a global state handler. This could be achieved by using Context API natively provided by React 16.x.

According to the documentation, Context API is for avoiding prop drilling which means passing a prop down to deeply nested components through all its parents. You read more about his here.

We will leverage the concept of the hook of React to make the consuming process of context more developer-friendly.

First and simple, we need to create a context. I usually create all files that relate to the global state inside the store folder that is in the src folder of the project.

|__src
    |__components
    |__store
        |__Context.ts
Enter fullscreen mode Exit fullscreen mode

The code for context will look like this.

export const Context = React.createContext(defaultContext)
Enter fullscreen mode Exit fullscreen mode

You can omit defaultContex, but it is good practice to use it in order to be able to isolate and test it.

So, now we have created our context. Let move to the main part which is creating an actual global state. There is nothing fancy here just a simple custom hook with your states. I usually call it useGlobalState.

|__src
    |__components
    |__store
        |__Context.ts
        |__useGlobalState.ts
Enter fullscreen mode Exit fullscreen mode

After creating the file we create the states that should be accessible from any component of our application and the methods to manipulate the state.

import { useState, useMemo } from "react";
import { makeApiRequest } from "../utils";

export const useGlobalState = () => {
  const [recipeList, setRecipeList] = useState(null);
  const [reviewBarOpen, setReviewBarOpen] = useState(false);
  const [loading, setLoading] = useState(true);

  const searchByName = useMemo(
    () => (keyword: string) => {
      makeApiRequest(
        `/api/search-by?keyword=${keyword}`,
        (data) => setRecipeList(data.meals),
        setLoading
      );
    },
    [setLoading]
  );

  const searchByIngredients = useMemo(
    () => (ingredients: string) => {
      makeApiRequest(
        `/api/filter-by?filterType=i&filterValue=${ingredients}`,
        (data) => setRecipeList(data.meals),
        setLoading
      );
    },
    [setLoading]
  );

  const openReviewBar = useMemo(() => () => 
                          setReviewBarOpen(true), [
                          setReviewBarOpen,
                        ]);

  const closeReviewBar = useMemo(() => () => 
                          setReviewBarOpen(false), [
                          setReviewBarOpen,
                        ]);
  const resetReviewState = useCallback(() => {
                            setReviewedRecipe(null);
                            closeReviewBar();
                           }, [closeReviewBar]);
  return {
    recipeList,
    searchByName,
    searchByIngredients,
    reviewBarOpen,
    resetReviewState,
  };
};

Enter fullscreen mode Exit fullscreen mode

So, basically, what we are doing is exposing only those parts of the state and methods that should be accessible publically from child components.

The next step is optional but make this solution more elegant. I create an additional provider component.

|__src
    |__components
    |__store
        |__Context.ts
        |__useGlobalState.ts
        |__StateProvider.ts
Enter fullscreen mode Exit fullscreen mode
import React from "react";
import { Context } from "./Context";
import { useGlobalState } from "./useGlobalState";

export const StateProvider: React.FC = ({ children }) => {
  const store = useGlobalState();

  return (
     <Context.Provider value={store}>
        {children}
     </Context.Provider>
  )
};

Enter fullscreen mode Exit fullscreen mode

Next, I wrap my application to the StateProvider, If not I cannot access global in children components.

import React from "react";

export const App= ({children})=>{
 return (
    <StateProvider>
      <Layout>
        {children}
      </Layout>
    </StateProvider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Lastly, I implement a custom hook to consume the global state.

|__src
    |__components
    |__store
        |__Context.ts
        |__useGlobalState.ts
        |__useStateProvider.ts
        |__useStore.ts
Enter fullscreen mode Exit fullscreen mode
import { useContext } from "react";
import { Context } from "./Context";

export const useStore = () => {
  const store = useContext(Context);
  return store;
};
Enter fullscreen mode Exit fullscreen mode

That's it, our global state is ready to use. Now, you just need to call the hook and consume the provided API.

import React, { useEffect } from "react";
import { useStore } from "@/store";

export const ByName: React.FC = () => {
  const { searchByName, getRandomRecipes } = useStore();
  const [value, setValue] = useState("");

  useEffect(() => {
    if (!Boolean(value.trim())) {
      getRandomRecipes();
    }
  }, [value, getRandomRecipes]);

 ...
Enter fullscreen mode Exit fullscreen mode

As a result, This keeps your components clean, only one place to look for bugs regarding your global state, and, also, isolates the data layer from the view layer which makes it easy to test this kind of application.

Testing

If you wonder how you would test your components that consume global state directly check out my other post where I walk you through process.
Let me know what's your implementation of the global state.

By the way, if you wanted to check the app where I implemented this style you can view it here and source code here.

Thanks for reading.

Discussion (0)