Global state with standard react hooks

Global state with react hooks

Posted on by Petter Kjelkenes - last updated 02. July, 2019

Today we will take a look at how we can implement simple global state using useContext and useReducer hooks.

We don't need redux to implement global state. That said, redux has some nice features that we wont have using the method i am about to show.

If you want to follow along use npx create-react-app.

We will start by creating a HoC withGlobalContext and a hook useGlobalState. withGlobalContext is supposed to be called once, and preferably on the top component. useGlobalState is a hook that we can use anywhere on all of our components.

src/useGlobalState.js

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

const GlobalStateContext = React.createContext();

export function withGlobalContext(WrappedComponent, initialStateFunc = () => {}, reducer) {
  return (props) => {
    const [state, dispatch] = useReducer(reducer, initialStateFunc());
    return (
      <GlobalStateContext.Provider value={[state, dispatch]}>
        <WrappedComponent {...props} />
      </GlobalStateContext.Provider>
    )
  }
}

export default function useGlobalState() {
  return useContext(GlobalStateContext);
}

This is the magic of hooks! Here we have created two functions

  • A HoC: withGlobalContext
    Which we will use on the App component.
  • A hook: useGlobalState
    Which is just a wrapper around useContext.

Lets create an App component and also 3 components that uses global state.

src/App.js

import React, { useState } from 'react';
import { withGlobalContext } from './useGlobalState';
import CounterComponent from './components/CounterComponent';
import MyNameComponent from './components/MyNameComponent';
import AsyncComponent from './components/AsyncComponent';

function App() {
  const [showAsyncComponent, setShowAsyncComponent] = useState(true);

  return (
    <div>
      <CounterComponent />
      <MyNameComponent />
      <button onClick={() => setShowAsyncComponent(!showAsyncComponent)}>Toggle async component</button>
      {showAsyncComponent && (
        <AsyncComponent />
      )}
    </div>
  );
}

function reducer(state, action) {
  switch (action.type) {
    case 'load-apis-fail':
      return {
        ...state,
        apis: [],
        apisIsLoading: false,
      };
    case 'load-apis':
      return {
        ...state,
        apis: [],
        apisIsLoading: true,
      };
    case 'set-apis':
      return {
        ...state,
        apis: action.payload,
        apisIsLoading: false,
        apisIsAlreadyLoaded: true,
      };
    case 'set-name':
      return {
        ...state,
        name: action.payload,
      };
    case 'clear-name':
      return {
        ...state,
        name: '',
      };
    case 'increment':
      return {
        ...state,
        counter: state.counter + 1
      };
    case 'decrement':
      return {
        ...state,
        counter: state.counter - 1
      };
    default:
      throw new Error();
  }
}

export default withGlobalContext(App, () => {
  // Can be dynamic setup logic here..
  return {
    counter: 0,
    name: '',
    apis: [],
    apisIsLoading: false,
    apisIsAlreadyLoaded: false,
  }
}, reducer);

We use our HoC withGlobalContext.

Here we send:

  • The default global state.
  • A reducer function for the global state.

src/actions/index.js

export const setNameAction = (name) => ({type: 'set-name', payload: name});
export const clearNameAction = () => ({type: 'clear-name'});

export const setApis = (payload) => ({type: 'set-apis', payload});

export const loadApis = () => ({type: 'load-apis'});

export const loadApisFail = () => ({type: 'load-apis-fail'});

We will be using these actions through the test components we will create.

src/components/CounterComponent.js

import React from 'react';
import useGlobalState from '../useGlobalState';

const CounterComponent = () => {
  const [globalState, dispatch] = useGlobalState();

  return (
    <h2 style={{display: 'flex'}}>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <div style={{width: '30px'}}>
        {globalState.counter}
      </div>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </h2>
  )
}

export default CounterComponent;

The counter component uses our custom hook "useGlobalState". This hook returns the globalState object and a dispatch method so that we can send actions to change the global state.

src/components/MyNameComponent.js

import React, { useState } from 'react';
import useGlobalState from '../useGlobalState';
import { setNameAction, clearNameAction } from '../actions';

const MyNameComponent = () => {
  const [globalState, dispatch] = useGlobalState(); // global state.
  const [name, setName] = useState(''); // local state name variable

  return (
    <div>
      <h3>My name is {globalState.name}</h3>
      <p>
        <input type="text" value={name} onChange={e => setName(e.currentTarget.value)} />
        <button onClick={() => dispatch(setNameAction(name))}>
          Set name
        </button>
        <button onClick={() => dispatch(clearNameAction())}>
          Clear name
        </button>
      </p>
    </div>
  )
}

export default MyNameComponent;

MyNameComponent also uses global state. This component has the ability to update the name and clear the name of the global state.

src/components/AsyncComponent.js

import React, { useEffect } from 'react';
import useGlobalState from '../useGlobalState';
import axios from 'axios';
import {loadApis, loadApisFail, setApis} from '../actions';

const AsyncComponent = () => {
  const [globalState, dispatch] = useGlobalState();
  const apis = globalState.apis;
  const isLoading = globalState.apisIsLoading;

  useEffect(() => {
    if (globalState.apisIsAlreadyLoaded) {
      return;
    }
    dispatch(loadApis());
    axios.get('https://api.met.no/weatherapi/available.json').then((response) => {
      setTimeout(() => {
        dispatch(setApis(Object.values(response.data)));
      }, 500);
    }).catch(() => {
      dispatch(loadApisFail());
    })
  }, [dispatch, globalState.apisIsAlreadyLoaded]);

  console.log(globalState);
  return (
    <div>
      {isLoading && (
        <p>
          Loading forecast api list..
        </p>
      )}
      {!isLoading && (
        <ul>
          {apis.map(api => (
            <li key={api.name}>{api.name}</li>
          ))}
        </ul>
      )}
    </div>
  )
}

export default AsyncComponent;

AsyncComponent uses the global state. It fetches some values from a API then dispatches actions.

Comments