React Context for state management

October 05, 2020

Directory and files setup

  • Create a context directory under src
  • create a resource specific subdirectory under context, e.g.: github
  • in the github folder create 3 files

    • githubReducer.js - to manage our state based on the actions fired
    • GithubState.js - to initialize a new context and store inital state and actions
    • type.js - to store the action names we will be able to fire to manipulate state

    Context folder Setup

Create GithubState.js and types.js

Create a file type.js in src/context

type.js will hold the list of actions that we plan to do, it’s basically a map to our planned actions.

Source code of types.js:

export const SEARCH_USERS = "SEARCH_USERS"
export const GET_USER = "GET_USER"
export const CLEAR_USERS = "CLEAR_USERS"
export const GET_REPOS = "GET_REPOS"
export const SET_LOADING = "SET_LOADING"
export const SET_ALERT = "SET_ALERT"
export const REMOVE_ALERT = "REMOVE_ALERT"
export const SET_TIMEOUTID = "SET_TIMEOUTID"

Then create GithubState.js in src/context/github, this will

  • create our context
  • initialize our state
  • define actions on state
  • return a context provider component, this will wrap our entire application

Source code of GithubState.js:

import React, { useReducer } from 'react'
import axios from 'axios'
import GithubReducer from './githubReducer'
import { createContext } from 'react'
import {
    SEARCH_USERS,
    SET_LOADING,
    CLEAR_USERS,
    GET_USER,
    GET_REPOS
} from '../types'

export const GithubContext = createContext()

const GithubState = (props) => {
    const initialState = {
        users: [],
        user: {},
        repos: [],
        loading: false
    }

    const [state, dispatch] = useReducer(GithubReducer, initialState)

    // Search Users

    // Get User

    // Get repos

    // Clear users

    // Set Loading

    return (
        <GithubContext.Provider
            value={{
                users: state.users,
                user: state.user,
                repos: state.repos,
                loading: state.loading
            }}
        >
            {props.children}
        </GithubContext.Provider>
    )
}

export default GithubState

Setup App.js for being a Context provider

Wrap the entire App.js with the GithubState context provider that we just created. First, of course you’ll need to import it.

Here is the stub from App.js:

...
import GithubState from './context/github/GithubState'
...
  return (
    // wrap our entire App.js with GithubState context
    <GithubState>
      <Router>
        <NavBar title='Home' icon='fas fa-home' />
        .
        .
        .
      </Router>
    </GithubState>
  )
}
export default App

Setup of searchUser action to update context state

We are now going to setup our searchUser function to apply the result of the user search functionality to the context state.

In GithubState.js add

  • the searchUser arrow function that’s going to perform the api call to github and dispatch the state management action to the reducer
  • register the function in the return of GithubContext.Provider
...
const GithubState = (props) => {
  const initialState = {
    users: [],
    user: {},
    repos: [],
    loading: false,
    tid: -1,
  }

  const [state, dispatch] = useReducer(GithubReducer, initialState)

  // Search Users
  const searchUser = async (qry) => {

    const res = await axios.get(
      "https://api.github.com/" +
        (qry && "search/") +
        `users?client_id=${process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${process.env.REACT_APP_GITHUB_SECRET}&q=${qry}`
    )
    dispatch({
      type: SEARCH_USERS,
      payload: qry === "" ? res.data : res.data.items,
    })
    .
    .
    .
      return (
    <GithubContext.Provider
      value={{
        users: state.users,
        user: state.user,
        repos: state.repos,
        loading: state.loading,
        searchUser,
      }}
    >
      {props.children}
    </GithubContext.Provider>
  )
  .
  .
  .
  }
  ...

Define the reducer

It’s time to create our reducer, that’s reponsible for manipulating our state. Here is the source of githubReducer.js, which is also created in /src/context/github:

import {
  SEARCH_USERS,
  SET_LOADING,
  CLEAR_USERS,
  GET_USER,
  GET_REPOS,
  SET_TIMEOUTID,
} from "../types"

export default (state, action) => {
  switch (action.type) {
    case SEARCH_USERS:
      return {
        ...state,
        users: action.payload,
        loading: false,
      }
    default:
      return state
  }
}

What happens is that by using the spread operator we populate the return object with current state, then overwrite with whatever we received from the dispatcher (from GithubState searchUser) as the action payload.

Now, wherever we need to use the context’s searchUser function, we need to use the useContext react hook as follows e.g.:

import React, { useContext } from "react"
import { GithubContext } from "../../context/github/GithubState"

const Search = (props) => {
  const githubContext = useContext(GithubContext)

  const submitter = (e) => {
    e.preventDefault()
    githubContext.searchUser(e.target.value)
  }
.
.
.

With this we centralized our state and state management function. When the searchUser function is run, it’s going to read from the api and dispatch the action to the reducer with the axios result set as a payload. The reducer will receive the action request, map the action (switch) and update the context’s state. If we want to reach and read from the context, we can do so again with useContext and simply refer to the state object attributes (e.g.:githubContext.users), for instance:

import React, { useContext } from "react"
import Spinner from "../layout/Spinner"
import UserItem from "./UserItem"
import { GithubContext } from "../../context/github/GithubState"

const Users = () => {
  const githubContext = useContext(GithubContext)
  return (
    <div style={style}>
      {githubContext.loading ? (
        <Spinner />
      ) : (
        githubContext.users.map((user) => (
          <UserItem key={`${user.login}-${user.id}`} user={user} />
        ))
      )}
    </div>
  )
}

const style = {
  display: "grid",
  gridTemplateColumns: "repeat(3, 1fr)",
  gridGap: "1rem",
}

export default Users

Well, there is a bit of a boilerplate code we need to write, plus you’ll need to get your head around the process flow, but once you got it, state management in React with context is a piece of cake. emoji-thumbsup