[React] Manage global state using Hooks and Context API without Redux

November 24, 2021

programming
article cover

Without Redux!

Redux used to be inseparable from React and has been used together so long to manage the global state. React Hooks was the game-changer but some people or some projects are still using Redux. Actually, there still are some advantages of using Redux, it has its own useful dev support libraries. But this time, let’s stay away from Redux and achieve full control of global state with Hooks and Context API.

What we're gonna make

We’re gonna create a typical number counter which is able to increment or decrement, also a text input that input value will be stored as global state. Since those are global state, they should be accessible from anywhere no matter to component hierarchy and be updated from anywhere.

screen captcha

Make Context

First, we create Context and Global state and pass Global state to the entire app using Context.Provider so that it can be accessed from anywhere in the app.

// store.js

import React, { createContext } from 'react'

const initialState = {
  count: 0,
  text: '',
}

export const Context = createContext(initialState) // <- initialize Context using initialState

export const Store = ({ children }) => {
  return (
    <Context.Provider value={[initialState]}> {/* <- this value is gonna be Global state */} 
      {children}
    </Context.Provider>
  )
}

Let’s say there are count and text in Global state. we can create Context using createContext, it takes initialState as an argument. then wrap the entire app with Context.Provider. The variable you pass to value is gonna be Global state.

Don’t forget to render children when you wrap the app.

// index.js

import React from 'react'
import ReactDOM from 'react-dom'

import App from './App'
import { Store } from './store'

ReactDOM.render(
  <Store>
    <App />
  </Store>,
  document.getElementById('root')
)

Use useContext when to access Global state. You can retrieve the state which you passed to value in Context.Provider.

// somewhere in your project 

import React, { useContext } from 'react'
import { Context } from './store'

export const Comp = () => {
  const [state] = useContext(Context) // <- retrieve the state from Context
  
  return <div>{state.count}</div>
}

Make Reducer

Now we can access Global state but still want to update Global state. Then it’s time Reducer comes. Reducer’s job is that receive dispatched action and return new state. to be exact, return new state according to an order in action.

// store.js

import React, { createContext, useReducer } from 'react';

const initialState = {
  count: 0,
  text: '',
};

export const Context = createContext(initialState);

export const Store = ({ children }) => {
  const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case 'increment':
        return { ...state, count: state.count + 1 }
      
      case 'decrement':
        return { ...state, count: state.count - 1 }
      
      case 'update_text':
        return { ...state, text: action.payload }
      
      default:
        return state
    }
  }, initialState);

  return (
    <Context.Provider value={[state, dispatch]}>
      {children}
    </Context.Provider>
  )
}

Let’s look close at useReducer first. It takes reducer and initialState as arguments then return state and dispatch.

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

This state here is what we should pass it to the entire app as Global state. And dispatch is used to dispatch action. We also need to pass dispatch to the entire app to dispatch action anywhere in the app.

Before look at Reducer, we need to know what action is like. The following is the typical action.

const action = {
  type: 'update_count',
  payload: 123
}

type specifies what kind of action we want reducer to do. payload is the value of the action. Now let’s look at reducer.

const reducer = (state, action) => {
  switch (action.type) {
    case 'update_count':
      return { ...state, count: action.payload }
    
    default:  
      return state
  }
}

reducer takes current state and dispatched action as arguments then return new state according to action. switch() statement is usually used to handle this. Make sure to return state as default in case that action doesn’t match any of cases.

How to use

If you are new to Global state management , the concept of dispatching action might be a bit tricky. But it’s quite simple actually, retrieve state and dispatch from Context using useContext(Context), and dispatch action anywhere and anytime you want.

// app.js

import React, { useContext, useState } from 'react'
import './style.css'
import { Context } from './store'
import { ChildComponent } from './child'

const App = () => {
  const [text, setText] = useState('')
  const [state, dispatch] = useContext(Context)

  const updateText = () => {
    dispatch({ type: 'update_text', payload: text })
    setText('')
  }

  return (
    <div>
      <h1>Global State without Redux!</h1>
      
      <section>
        <p>You can increment/decrement count</p>
        <p>count: {state.count}</p>
        <button onClick={() => dispatch({ type: 'increment' })}>+</button>
        <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      </section>
      
      <section>
        <p>text: {state.text}</p>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
        <button onClick={updateText}>update text</button>
      </section>

      <ChildComponent />
    </div>
  )
}

export default App

Done! Now, parent component and child component, they both can access Global state and update it.

screen captcha

Code


Profile picture

Photographer / Web engineer working in Berlin.
A Japanese living with a Slovak wife and two German cats.