+

Global state management with react hooks and context

24 Sep 2019

It all started with amazing frameworks like react, vue, angular and some others that have had the brilliant idea of abstracting the application data from the document object model (DOM). React specifically, with your reconciliation algorithm and soon with the fiber architecture, rocks on how fast these layers (abstraction and DOM) are updated. With that we can focus on our components instead of the “real” HTML implementations, however from that also come some other new challenges, let’s put it in images:

Prop drilling

That’s the classical prop drilling react anti-pattern, the process of going through the react component tree in order to pass properties between them. Higher order components or Decorators, if you are in a more object oriented style, give us more flexibility and some others architectural possibilities. We can now extract away that functionality which we want to share and decorate the components that need use it.

High Order Component

It’s all fine while dealing with small apps with few components interacting with each other, however when we have complex communication between a vast component ecosystem than this approach starts getting complicated and bug prone. From that reality our unidirectional data flow comes into stage:

react-redux

Until here nothing new, but what if we take the concept and apply it using react context and hooks!? That’s why you here!

Main concept

The main highlight by now is our great and new friend react hooks, and your consequently functional approach:

Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.

Then the center idea is to use the context API together with useContext and useReducer hooks to make our store available to our components.

import React, { createContext, useContext, useReducer } from 'react';
 
export const StateContext = createContext();
 
export const StoreProvider = ({ reducer, initialState, children }) => (
  <StateContext.Provider
    value={useReducer(reducer, initialState)}
    children={children}
  />
);
 
export const useStore = () => useContext(StateContext);

We export from this file source code here a StoreProvider (responsible for making the context/store available in the application), that receives:

  • the reducer function with the signature (state, action) => newState;
  • application initialState;
  • and the application content (children);

And the useStore hook that is responsible for getting the data from the store/context.

Even though the nomenclatures are different from now on I’ll reference our context as store, because the concept is the same and we can easily associate to our well known redux architecture standard.

The beauty relies on this simplicity:

  1. StateContext.Provider receives a value object (your current state);
  2. useReducer receives a function: (state, action) => newState and an initialState then any dispatch made from our app will pass here and update our application current state;
  3. useContext get our store and make it available in our application!

All the rest is just code organization and minor changes, nothing to worry about :)

Going into details

As a proof of concept, I’ve done this basic todo list application, check here the source code and here the live implementation, it’s a basic interface that contains a couple of component and the current state tree so then we can see the state modifications over the time.

The project structure looks like this:

Project folder structure

The structure is pretty straightforward (action as we’d normally do in a redux application), I’ve moved the initialState from the reducers, because reducer is about state modification and not definition, besides that the store folder contains the already discussed react context / hooks implementation.

The reducer file has a quite different design:

import * as todo from './todo';
import * as types from 'actions/types';
 
const createReducer = handlers => (state, action) => {
  if (!handlers.hasOwnProperty(action.type)) {
    return state;
  }
 
  return handlers[action.type](state, action);
};
 
export default createReducer({
  [types.ADD_TODO]: todo.add,
  [types.REMOVE_TODO]: todo.remove,
  [types.UPDATE_TODO]: todo.update,
  [types.FILTER_TODO]: todo.filter,
  [types.SHOW_STATE]: todo.showState,
});

The point here is just to avoid those huge switch statements usually seen in reducer functions with a mapping object, so basically for every new reducer we just add a new entrance in the mapping object.

But again, it’s all a matter of implementation the requirement here is that the function needs to have the (state, action) => newState interface as we’re already used to with Redux.

And finally but not least our component subscribing to the store:

import React from 'react';
 
import { useStore } from 'store';
import { addTodo, filterTodo } from 'actions';
 
import uuid from 'uuid/v1';
 
import Button from '@material-ui/core/Button';
 
export default props => {
  const [{ filter }, dispatch] = useStore();
 
  const onClick = () => {
    dispatch(addTodo({ id: uuid(), name: filter, done: false }));
    dispatch(filterTodo(''));
  };
 
  return (
    <Button
      {...props}
      variant='contained'
      onClick={onClick}
      disabled={!filter}
      children='Add'
    />
  );
};

What comes next

The next steps will be related to middlewares and type-checking, how do we work here? Technically the middleware is a function called just before the dispatched action reaches the reducer, so the createReducer function above is a great place for that, and what about type-checking!? Typescript on it! And see you soon!

Cheers!

References: https://github.com/acdlite/react-fiber-architecture https://reactjs.org/docs/reconciliation.html https://reactjs.org/docs/hooks-intro.html https://github.com/vanderleisilva/react-context


... Contact ...

Ganghoferstraße 39, 80339 Munich - Germany
Email: vanderlei.alves.da.silva@gmail.com
Telephone: +49 17686314967