React - Manage state using context API with useState or useReducer hooks

React - Manage state using context API with useState or useReducer hooks

State Management

In any react application there are different parts of the UI that are separated in different components. Some components may need to use a state declared or updated on another component. Traditional way to do this was to create a state in the parent component and pass state as props to the child component. This works but for the applications with multiple levels of the nested children will require to pass props to each nested child. This process is called props drilling.

What is prop drilling?

Prop drilling refers to the passing of the data from the parent to all the nested children in the React Tree.

This works until when we need to change the code

  1. Updating the prop value for instance children components called <Child1 state=”data” /> and <Child2 state=”data” />, when we need to update this component for instance changing state from string value to array value like state=[“data1”, “data2”] will require updating all the child component props.
  2. Let assume that in the state of application there comes a time to rename the prop as you are removing some data or passing more. It has to be changed at all the places down the hierarchy.

To solve this problem, one needs to have a proper tool to handle state across the application, there are multiple options such as redux, MobX, Flux and context API. In this article you will learn how to use context api with usestate and usereducer, simple to use and lightweight solution.

Context API

“Context provides a way to pass data through the component tree without having to pass props down manually at every level.” — React

Context api is a built-in react hook.

When to Use Context

Context is designed to share data that can be considered global for a tree of React components, such as the current authenticated user, theme, or preferred language.

Context Api uses two main hooks (createContext and useContext), along with one hook to set and update state{this hook isn't a must but it is important for state updates}.

Let's use both useState hook and useReducer hook just to get a general idea on what context is capable of. We’ll use a light/dark mode toggle component for all our examples.

Context Api with useState

Create a jsx file context.jsx and start editing,

  • Create the context and give it a name, context name can be any name, for this example we’ll use StateContext.
  • Create a state provider passing children as props (children can be anything you pass, and in our case it is the whole app, this also means this is a react component and we gonna wrap it on our application
  • Lastly declare a method to use our context
import React, { createContext, useContext, useState } from "react"

// create toggle context
const ToggleContext = createContext()

// create context provider
export const ToggleProvider = ({ children }) => {
    const [data, setData] = useState({
       darkMode: false,
    })
    // the value passed in here will be accessible anywhere in our application 
    // you can pass any value, in our case we pass our state and it's update method 
    return (
        <ToggleContext.Provider value={{data, setData}}>
            {children}
        </ToggleContext.Provider>
    )
}

// useToggleContext will be used to use and update state accross the app
// we can access to data and setData using this method 
// anywhere in any component that's inside ToggleProvider
export const useToggleContext = useContext(ToggleContext)
  • Let's use it now, In your root file ie App.jsx import StateProvider and wrap your app in it.

Wrapping our components inside the provider will give all children access to the state declared in that provider. Any component outside the wrapper won’t have access to the global state.

// import provider from context jsx
import { ToggleProvider } from "./context"
import Home from "./Home"

function App() {
    // Wrap the all components inside provider
    return (
        <ToggleProvider>
            {/* every other component */}
            <Home />
        </ToggleProvider>
    )
}

export default App
  • Now that state is global, let's use it. \ Create any files lets say Nav.jsx and Home.jsx. In these files import useStateContext from context.jsx, lets see it in action
// Nav.jsx

import { useToggleContext } from "./context"

const Nav = () => {
    // declare state just like you did in context jsx
    // But instead of useState, use useToggleContext
    const [data, setData] = useToggleContext()
    return (
        <div>
            <button 
                onClick={() => setData({
                    ... data,
                    darkMode: !data.darkMode
                  })}
            >
              {data.darkMode ? "Change to Light Mode" : "Change To Dark Mode"}
            </button>
        </div>
    )
}
// Home.jsx

import { useToggleContext } from "./context"
import Nav from "./Nav"

const Home = () => {
    // declare state just like you did in context jsx
    // But instead of useState, use useToggleContext
    const [data, setData] = useToggleContext()
    return (
        <div
          style={{
              // update mode between dark and light
            backgroundColor: data.darkMode ? "#000000" : "#ffffff",
            color: data.darkMode ? "#ffffff" : "#000000" 
          }}
        >
          <Nav />
        </div>
    )
}
  • On toggle button click state will be changed as well as web styles.

Now we have our state. You can use this state in any component, data can be used as a value and setData to update data.

Context API with useReducer

useReducer

useReducer is one of the hooks that helps in managing states. You can use this hook as a replacement for useState, doesn't necessarily require context api, it's a standalone hook.

How does it work?

For react reducer to work we need an initial state, reducer function and dispatch to update data.

Before diving into the context side of useReducer, let's explore it when used as a standalone hook.

import React, { useContext, createContext, useReducer } from "react"

// context for using state
const ToggleStateContext = createContext()

// context for updating state
const ToggleDispatchContext = createContext()

// reducer function
const reducer = (state, action) => {
  const { type, payload } = action
      case: "CHANGE_MODE":
          return {
          ...state,
          darkMode: payload
        }
      default:
        return state
  }
}


export const ToggleProvider = ({ children }) => {
    const [state, dispatch] = useReducer(reducer, {
      darkMode: false
    })

    return (
      <ToggleDispatchContext.Provider value={dispatch}>
          <ToggleStateContext.Provider value={state}>
             {children}
          </ToggleStateContext.Provider>
      </ToggleDispatchContext.Provider>
    )
}

// use them context we've created
export const useToggleStateContext = () => useContext(ToggleStateContext)
export const useToggleDispatchContext = () => useContext(ToggleDispatchContext)

useReducer with Context API

Now that we have managed to use the useReducer hook in a component, let's use it for context api, shall we? As I said earlier, context api required a hook to set and update state, so as we implemented useState, we are going to implement useReducer the same, let's get into it.

For this we gonna create two context, one for dispatch and another for state in order to pass state and dispatch values differently

  • Create a context provider in context.jsx

Here reducer function is the same to the one we used in Nav.jsx

// import provider from context jsx
import { ToggleProvider } from "./context"
import Home from "./Home"

function App() {
    // Wrap the all components inside provider
    return (
        <ToggleProvider>
            {/* every other component */}
            <Home />
        </ToggleProvider>
    )
}

export default App
  • So we have our context api with useReducer, lets go to the next step, wrapping our app in Context provider
// import provider from context jsx
import { ToggleProvider } from "./context"
import Home from "./Home"

function App() {
    // Wrap the all components inside provider
    return (
        <ToggleProvider>
            {/* every other component */}
            <Home />
        </ToggleProvider>
    )
}

export default App
  • Now we have our state globally available, lets go and use or update it somewhere, say Nav page as we did on useState example.
// Nav.jsx
import React from "react"
import { useToggleDispatchContext, useToggleStateContext } from "./context"

const Nav = () => {
      const { darkMode } = useToggleStateContext()
      const dispatch = useToggleDispatchContext()

    return (
        <div>
        {/* this will update the specific state by checking the type */}
        <button onclick={() => dispatch({
            type: "CHANGE_MODE",
            payload: !darkMode
          })}>
            {darkMode ? "Change To Light Mode" : "Change to Dark Mode"}
        </button>
        </div>
    )
}

And in Home file

// Home.jsx
import { useToggleStateContext } from "./context"
import Nav from "./Nav"

const Home = () => {
    const { darkMode } = useToggleStateContext()
    return (
        <div
          style={{
              // update mode between dark and light
            backgroundColor: data.darkMode ? "#000000" : "#ffffff",
            color: data.darkMode ? "#ffffff" : "#000000" 
          }}
        >
          <Nav />
        </div>
    )
}

And things should work as expected but now with global state which you can use anywhere

useState VS useReducer in Context API

As much as useState looks cleaner and simpler to implement, for large applications with a lot of state change, useReducer will give you more control on your state.

When not to use Context API

When an application requires a lot of state updates, when state changes, all the children using the same provider will rerender whether they use the updated state or not.

Redux and other third party state management libraries solve this issue. It's a matter of deciding whether you really need to use an extra library or not depending on how large your application is and how much state will be updating that will require global state management.

If you like this article there are more like this in our blogs, follow us on dev.to/clickpesa, medium.com/clickpesa-engineering-blog and clickpesa.hashnode.dev

Happy Hacking!!