FSociety

An experiment to replace Redux with Hooks

February 06, 2019

It’s not a long time ago since React Core team introduced Hooks as an experimental proposal to the React ecosystem. Even though it was purely demonstrated as experimental, the amount of confidence the community put into this feature was enough to push it to the production-ready state as fast as possible.

Initially, we were thinking about it as a way to not write Class components and that means the use case of internal states and event handlers. Redux uses wrapper HoCs to inject the state to any components (which can be a functional component) so that shouldn’t be an obvious replacement for Hooks. This article’s aim is only to provide a proof of concept for this implementation and is not trying to say that this is the suggested implementation.

Which Hooks are we using?

The combination of two Hooks which both are distributed alongside the React package.

useReducer

This Hook receives two parameters, a reducer very similar to a Redux reducer and an initial value.

const initialValue = 0;
const reducer = (state, action) => return state;

const [state, dispatch] = useReducer(reducer, initialValue);

useContext

This one do accept a Context type and will return the value of that Context which in essense makes this:

return (
  <YourContext.Consumer>
    {value => <div>The context value is {value}.</div>}
  </YourContext.Consumer>
);

Like this ✌:

const value = useContext(YourContext);

return (
  <div>The context value is {value}.</div>
);

How to do it?

The idea is to handle the state using useReducer hook and send the state and dispatch function down the component tree using a ContextProvider. We will receive the value of state and dispatch in any component using a useContext hook which is explained above.

The only problem here is that in Redux we can have multiple reducers to handle different keys on the state but the useReducer hook only accepts one reducer. So we need to find a way to combine the reducers before feeding them into this hook.

The solution we have for this problem is no different than Redux. We should use a function called combineReducers to accept an object of keys with reducers functions like this:

// reducers/index.js
combineReducers({
  wasted: wastedReducer,
  beers: beerReducer
});

This function after execution should return another function which is similar to a normal reducer, but after getting the state and action will enumerate all the keys in the object above and calculate the result of each reducer based on the action and returns the whole state. We can just import the one from redux but I thought we are supposed to be independent! So… I rewrite that function in a simplified manner. It’s basically the same thing but worse! Because it has less error handling so I don’t really recommend it for production:

// reducers/combineReducers.js
function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers);

  return function combineReducers(state = {}, action) {
    const nextState = {};
    let hasChanged = false;
    // calculate the next state
    reducerKeys.forEach(key => {
      const reducer = reducers[key];
      const previousStateForKey = state[key];
      const nextStateForKey = reducer(previousStateForKey, action);
      if (typeof nextStateForKey === "undefined") {
        throw new Error(
          `You already started drinking? Your reducer for ${key} is returning undefined!`
        );
      }
      nextState[key] = nextStateForKey;
      if (nextStateForKey !== previousStateForKey) {
        hasChanged = true;
      }
    });

    // Not the best approach, we need middlewares later
    console.log("State ", state);
    console.log("Dispatching ", action);
    console.log("Next state ", nextState);

    return hasChanged ? nextState : state;
  };
}

Dispatching Actions

As described earlier we are sending state and dispatch function through a context provider like this:

// index.js
  const [state, dispatch] = useReducer(reducers, initialState);

  return (
    <StateContext.Provider value={{ state, dispatch }}>
      <AppContainer />
    </StateContext.Provider>
  );

So deep down the tree, we can use a hook like this to retrieve the state or dispatch:

// BeerManager.js
const { state, dispatch } = useContext(StateContext);

Having said so, in any component, dispatching an action should be as straighforward as this:

// BeerManager.js
dispatch({
  type: "ADD_BEER",
  beerType: "bottle"
});

For the sake of simplicity for this mini-project, I didn’t separate out the action objects into other files. You can always do that for your own project of course.

How it all works together?

I used CodeSandbox to build this experimental project. So feel free to check out the demo in my SandBox and play around with the code. You can also view or fork the code in Github 🤖 and submit pull requests.


Pooria Atarzadeh

Personal blog by Pooria Atarzadeh
Unattended curiosity on web, graphql, blockchain and more

About Me