DEV Community

Cover image for React Context APIs, State sharing and Zustand
Raju Sarkar
Raju Sarkar

Posted on

React Context APIs, State sharing and Zustand

In React, we typically use props to pass data from a parent component to its children component. Props passing works well for small applications, but in larger application, props passing become a pain — especially when data needs to be accessed by deeply nested components.

Lets imagine a scenario where you have three components — A, B(B is child of A), and C(child of B). If component C needs data from component A, you have to pass that data through B even though B does not actually need it. This makes your development a little bit complex and also, you have to compromise with the performance.

To solve this problem, React provides the Context API, which allows you to share values across different component, without having to pass props manually at every level.

Get started with Context API

Create a empty React project

npm create vite@latest
// Select React
// Then select typescript
Enter fullscreen mode Exit fullscreen mode

After this, create two folder inside the src — context and components.

Lets setup the context first.

//ThemeContext.tsx

import { createContext, useContext, useState, type ReactNode } from "react";

// theme context with default theme value `light`
// and a empty callback function to change theme
const ThemeContext = createContext({ theme: "light", toggle: () => {} });

// this is our theme provider
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
  // state variables for theme
  const [theme, setTheme] = useState("light");

  // theme toggle function
  const toggle = () =>
    setTheme((prev) => (prev === "light" ? "dark" : "light"));

  return (
    // wraping the children with context provider
    // and providing valus
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
};
// export the useTheme, so it can be consumed by components
export const useTheme = () => useContext(ThemeContext);
Enter fullscreen mode Exit fullscreen mode

In the above code we are importing — createContext, useContext, useState and type ReactNode from React.

import { createContext, useContext, useState, type ReactNode } from "react";

At first we need to initiate a context, So we are initiating ThemeContext with a default value light and a callback function, toggle to change the theme value, this will create a context that components can read from or update values of that context.

// theme context with default theme value
// and a empty callback function to change theme
const ThemeContext = createContext({ theme: "light", toggle: () => {} });
Enter fullscreen mode Exit fullscreen mode

Now we need a context provider, for that we are creating ThemeProvider, this is the component that will wrap a child component and give access to data, values to that child component.

// this is our theme provider
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
  // state variables for theme
  const [theme, setTheme] = useState("light");

  // theme toggle function
  const toggle = () =>
    setTheme((prev) => (prev === "light" ? "dark" : "light"));

  return (
    // wraping the children with context provider
    // and providing valus
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => useContext(ThemeContext);

Enter fullscreen mode Exit fullscreen mode

In the above code we are wrapping the children with our ThemeContext that we have created before.

And at last we are exporting the useTheme, so components can access the ThemeContext value without wrapping them by ThemeProvider.

Now lets actually use the ThemeProvider in our App.tsx. First lets create a component, ThemeSwitch.

// ThemeSwitch.tsx

// import the useTheme from our theme context
import { useTheme } from "../context/ThemeContext";

export default function ThemeSwitch() {
  // initiate
  const { theme, toggle } = useTheme();

  return (
    <div className="p-2">
      {/* on button click this will call the `toggle` function 
        and changes the theme */}
      <button onClick={toggle} className="border p-2 bg-amber-100 rounded">
        Theme switch
      </button>

      <p className="text-4xl">
        Current theme: <span className="underline text-blue-600">{theme}</span>
      </p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here we are importing the useTheme from our ThemeContext. Inside our ThemeSwitch initiate the useTheme(), and here we can get access of theme and toggle. On button click this triggers the toggle function that we have in our ThemeConntext and changes the theme.

Now in our App.tsx, clear all the boilerplate code, import the ThemeProvider and wrap the ThemeSwitch with that.

// App.tsx

// get the theme provider from theme context
import { ThemeProvider } from "./context/ThemeContext";
// get the theme switch component
import ThemeSwitch from "./components/ThemeSwitch";


const App = () => {
  return (
    <ThemeProvider>
      <ThemeSwitch />
    </ThemeProvider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Now run the code and you can see on clicking the button it changes the theme.

Changing color on button click

To demonstrate this further lets create another component, DemonstrateThemeChange.

// DemonstrateThemeChange.tsx

import { useTheme } from "../context/ThemeContext";

export default function DemonstrateThemeChange() {
  const { theme } = useTheme();

  return (
    <div className="p-2">
      <div
        className={`h-48 w-48 rounded flex justify-center items-center ${
          theme === "light" ? "bg-gray-300" : "bg-black text-white"
        }`}
      >
        <p>{theme}</p>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Component box color change

Here we are importing the useTheme and rendering css according to current theme. Now if you click the ThemeSwitch button you will see that the the DemonstrateThemeChange box color changes without any wrapping. This way you can eliminate props drilling

So, is Context API enough to manage states and share data across an React application? The answer is no.
Context API is great for smaller application to store one or two states, but when the application gets bigger we need a better approach, a better solution to manage states across our application.

Why Context is not recommended for large scale applications :
Re-Renders all consumers: Lets say, that you have context user, that stores user profile info and also user shopping cart details, now if that user add a new item to their cart, user’s shopping cart data will get updated, and this will trigger re-render for both shopping cart data consumer and user profile data consumer.
No built in selector: You can not subscribe to only one part of a context, like I want only user profile data not the shopping cart data. To do this you have split the context which adds complexity.
Provider hell: To deal with above problems, lets say that you have separated your context, but now guess what you have to do deep nesting.

<UserProvider>
  <ThemeProvider>
    <CartProvider>
      <App />  // 🤯 Too many wrappers!
    </CartProvider>
  </ThemeProvider>
</UserProvider>

Enter fullscreen mode Exit fullscreen mode

To solve this we have dedicated state management tools like Redux/Zustand.
By using state management tools like Redux/Zustand you can avoid Unnecessary re-render, provider hello and you can also get built-in optimization.

Get Started with Zustand

Zustand is a small, fast, and scalable state management solution.

To use Zustand you have to install it:

npm install zustand

After installation create a store folder in the src of your project directory and inside that store folder create a useUserStore.ts. Inside our user store file paste:

import { create } from "zustand";
import { persist } from "zustand/middleware";

const useUserStore = create(persist((set) => ({}), {name: "user-store"}))
Enter fullscreen mode Exit fullscreen mode

Here, we are importing create from zustand to create the store, and then we are importing the persist to enable state persistence. You can create a zustand store without using persist, but in that case, the state will be lost when the page is refreshed. The third line is the boilerplate code for creating a zustand store.

If you prefer JS, your are good to go — you can now declare the states inside this useUserStore. Because I am using TS, I will first create a type interface and then use that interface in my store.

import { create } from "zustand";
import { persist } from "zustand/middleware";

// TYPE INTERFACE FOR SHOPPING CART
interface UserShoppingCart {
    productName: string,
    productPrice: string,
    productQty: number
}

// TYPE INTERFACE FOR USER
interface User {
    isLoading: boolean,
    isError: boolean,
    errorMessage: string | null

    // TO STORE VALUE
    isUserLogedIn: boolean,
    userShoppingCart: UserShoppingCart[]

    // FUNCTIONS TO CHANGE VALUE
    changeUserLogedInStatus: ({isLogedIn}:{isLogedIn: boolean}) => void
    addItemToUserShoppingCart: ({productName, productPrice, productQty} : {productName: string, productPrice: string, productQty: number}) => void
}

// OUR STORE
const useUserStore = create(persist<User>((set) => ({
    // PROVIDING THE STATE VARIABLE TO OUR STORE
    isLoading: false,
    isError: false,
    errorMessage: null,

    isUserLogedIn: false,
    userShoppingCart: [],

    changeUserLogedInStatus : ({isLogedIn}) => {},
    addItemToUserShoppingCart: ({productName, productPrice, productQty}) => {},
}), {name: "user-store"}))

// EXPORT THE STORE 
export { useUserStore }

Enter fullscreen mode Exit fullscreen mode

So, above I introduced two type interface, User and UserShoppingCart. These ensures type safety for our state data across different components. After that we provide the state variable to our store with initial values.

Here you can declare as many state variables as you need, like —userSessionId or userShoppingHistory or anything else you need.

Now lets see how we can change value or update value in our store. Lets create two basic components, LoginButton and AddToCartButton — These will be just a normal button, whenever we click this buttons values into our store will get updated. The AddToCartButton will only shown to the user when the isUserLogedIn value is true.

So, lets first complete our store functions which will update our state value

const useUserStore = create(persist<User>((set) => ({
    isLoading: false,
    isError: false,
    errorMessage: null,

    isUserLogedIn: false,
    userShoppingCart: [],

    changeUserLogedInStatus : ({isLogedIn}) => {
        set({isUserLogedIn: isLogedIn})
    },
    addItemToUserShoppingCart: ({productName, productPrice, productQty}) => {
        set((prev) => ({
            userShoppingCart: [...prev.userShoppingCart, {productName, productPrice, productQty}]
        }))
    },
}), {name: "user-store"}))

Enter fullscreen mode Exit fullscreen mode

To update our store with new value we need use the set property. Those two functions are simple JS code, If you find it difficult to understand to can Google them.

Now lets create our component to update user login status.

import { useUserStore } from "../store/useUserStore"

export default function LoginButton(){
    const {changeUserLogedInStatus, isUserLogedIn} = useUserStore()
    return (
        <div className="flex justify-center items-center min-h-[20vh]">
            <button className="border bg-gray-300 active:bg-gray-400"
            onClick={() => {
                changeUserLogedInStatus({isLogedIn: !isUserLogedIn})
            }}
            >Update login status</button>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

I have created a LoginButton component in my component folder and there I have imported the changeUserLogedInStatus and isUserLogedIn(current user login status) from useUserStore and then I am updating the user login status to opposite of current value on button click.

Now remove everything from App.tsx return statement and add the below code.

import LoginButton from "./components/LoginButton";

const App = () => {
  return (
    <div>
      <LoginButton />
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Now go to localhost of your app and then your browser’s local storage section then click on the Update login status button and now you will see a new store added to your local storage with the updated userLogedIn status.

Zustand store in browser

Zustand store in browser

Now lets create the AddToCartButton component.

import { useUserStore } from "../store/useUserStore"

export default function AddToCartButton(){
    const {addItemToUserShoppingCart} = useUserStore()
    return (
        <div className="flex justify-center items-center min-h-[30vh]">
            <button
             className="border bg-blue-300 active:bg-blue-400"
             onClick={() => {
                addItemToUserShoppingCart({productName: "Ferrari Roma", productPrice: "4 CR", productQty: 1})
             }}
            >Add to cart</button>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Create the component in component folder and import the addItemToUserShoppingCart and then add the logic for adding items to shopping cart on button click. But we want this button to be visible only when the user is logged In. To do that we will update our App.tsx.

import AddToCartButton from "./components/AddToCartButton";
import LoginButton from "./components/LoginButton";
import { useUserStore } from "./store/useUserStore";

const App = () => {
  const {isUserLogedIn}=  useUserStore()
  return (
    <div>
      <LoginButton />

      {
        isUserLogedIn && (<AddToCartButton />)
      }
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Here we are rendering the AddToCartButton only when the user login status is true. Now if you click the Add to cart button this will add item to your shopping cart.

Change store value on button click

Change store value on button click

At the begging the user login status is false and when we click on the login button the status updated as true, now we can see the add to cart button. On clicking the add to cart button this will add the provided item into the shopping cart.

So this is how you can manage states store data and access them across different component using Zustand.

To learn more about Zustand you can refer to Zustand official docs.

Source code is available on GitHub. Also you can provide your feedback on Twitter/X.

Top comments (0)