Creating a react-redux alternative with Hooks and Proxies

Learn to create a simple React/Redux integration using Hooks, then improve the performance using JavaScript Proxies.

Ever since I first learned Redux, I’ve been thinking about alternative ways to integrate it with React. My intuition was that Redux is super simple (which I like a lot), whereas react-redux’s performance optimizations make it complex. And while I’d still recommend that you use the official react-redux library for business products, if you’re playing with a toy app or you’re just starting to learn Redux, then you might want something more straightforward.

The React team recently introduced the new Hooks API, allowing for the use of stateful logic in function components. In particular, this allows for custom hooks — reusable chunks of state management logic. As a result, there have been many discussions about how to create hooks-based React bindings for Redux, and many proposals that would replace Redux completely, including mine.

But today, I’ll discuss something simpler: instead of replacing Redux, I’ll demonstrate how to develop custom hooks for Redux in a straightforward way. I’ll first describe a naive implementation, and later introduce a Proxy-based approach that seamlessly improves performance.

#A naive implementation

Let’s start with a naive implementation. If you are not familiar with the Context and Hooks APIs, please visit the official docs to learn them first. The rest of this article assumes a basic understanding of them.

First, let’s create a single context that can pass around a Redux store.

const ReduxStoreContext = React.createContext();

const ReduxProvider = ({ store, children }) => (
  <ReduxStoreContext.Provider value={store}>
    {children}
  </ReduxStoreContext.Provider>
);

We then define two hooks: useReduxDispatch() and useReduxState(). The reason we have two separate hooks is that not all components will use both hooks at the same time, and we want to hide the usage of context within the implementation.

Incidentally, the implementation of useReduxDispatch() is very simple.

const useReduxDispatch = () => {
  const store = useContext(ReduxStoreContext);
  return store.dispatch;
};

The naive implementation of useReduxState() is a little more complex, and uses four hooks: useContext(), useRef(), useEffect() and useForceUpdate().

const useReduxState = () => { 
  const forceUpdate = useForceUpdate();
  const store = useContext(ReduxStoreContext);
  const state = useRef(store.getState());
  useEffect(() => {
    const callback = () => {
      state.current = store.getState();
      forceUpdate();
    };
    const unsubscribe = store.subscribe(callback);
    return unsubscribe;
  }, []);
  return state.current;
};

Basically, we just subscribe to any changes in the Redux store’s state. (A minor note: this doesn’t support changing the store on the fly, which may be important for testing.)

We can then implement useForceUpdate() as below.

const forcedReducer = state => !state;
const useForceUpdate = () => useReducer(forcedReducer, false)[1];

#A usage example

So how would you use useReduxState(), useReduxDispatch() and <ReduxProvider>? Take a look at this simple example to find out.

App.js
store.js
redux-bindings.js
index.js
styles.css
import React, { useCallback } from 'react';
import store from './store';
import { ReduxProvider, useReduxState, useReduxDispatch } from './redux-bindings';

const App = () => (
  <ReduxProvider store={store}>
    <Counter />
    <TextBox />
  </ReduxProvider>
);

const Counter = () => {
  const state = useReduxState();
  const dispatch = useReduxDispatch();
  const inc = useCallback(() => dispatch({ type: 'inc' }), []);
  return (
    <div>
      <div>Count: {state.count}</div>
      <button onClick={inc}>+1</button>
    </div>
  );
};

const TextBox = () => {
  const state = useReduxState();
  const dispatch = useReduxDispatch();
  const setText = useCallback(event => dispatch({
    type: 'setText',
    text: event.target.value,
  }), []);
  return (
    <div>
      <div>Text: {state.text}</div>
      <input value={state.text} onChange={setText} />
    </div>
  );
};

export default App;
Build In Progress

If you are familiar with Redux and react-redux, then you’ll probably notice some issues with the above example.

To start with, if you click the +1 button, not only does it re-render the Counter component — it also re-renders the TextBox component. Well, this is not a problem until it is a problem. For small apps, it works just fine. But for larger apps, it may cause problems with performance.

Really, we’d like to avoid unnecessary rendering if possible. If you were using react-redux, you might supply connect() with a mapStateToProps function to achieve this:

mapStateToProps: (state, ownProps?) => Object

mapStateToProps allows you to specify which part of the state will be used by a component. But actually defining it is a lot of extra work, and can be troublesome in some cases. It’s easy to add heavy computations in such functions, and I’ve found that beginners have trouble optimizing this due to lack of familiarity with memoization.

But what if you don’t need to specify a mapStateToProps function? What if you can use Proxy instead?

#Improving useReduxState() with Proxy

JavaScript’s new Proxy object makes it possible to automatically detect which part of the state is used during rendering. Then when your component is notified of a new state, it will know if the relevant part has changed, and can re-render only when necessary.

Let’s modify our useReduxState() hook to implement this feature. For now, we’ll only worry about the first level of the state object (and we’ll ignore state that is deep in the object tree).

const useReduxState = () => { 
  const forceUpdate = useForceUpdate();
  const store = useContext(ReduxStoreContext);
  const state = useRef(store.getState());
  const used = useRef({});
  const handler = useMemo(() => ({
    get: (target, name) => {
      used.current[name] = true;
      return target[name];
    },
  }), []);
  useEffect(() => {
    const callback = () => {
      const nextState = store.getState();
      const changed = Object.keys(used.current)
        .find(key => state.current[key] !== nextState[key]);
      if (changed) {
        state.current = nextState;
        forceUpdate();
      }
    };
    const unsubscribe = store.subscribe(callback);
    return unsubscribe;
  }, []);
  return new Proxy(state.current, handler);
};

Now, the TextBox will not be re-rendered when you click +1, and vice versa.

This implementation is somewhat limited, and you might think that the shallow comparison is not enough. Indeed, if you already have a concrete design for your state structure, then that may be the case. However, if you start designing a new state structure, you could design it with first-level separation in mind. Also, note that for complex structures where you would need reselect with react-redux, you could use useMemo() in a function component so that it returns a memoized element.

#The library

As it happens, I’ve developed a library called react-hooks-easy-redux that takes this approach. This library actually allows deep comparison thanks to proxyequal. (Still, the shallow comparison described in the previous section may work better in some use cases.)

View react-hooks-easy-redux at GitHub »

The repository contains several examples — here’s one that you can play with as a live Demoboard:

Counter.jsx
Person.jsx
App.jsx
state.js
index.js
import * as React from 'react'
import { useReduxDispatch, useReduxState } from 'react-hooks-easy-redux'
import { Action, State } from './state'

const Counter = () => {
  const state = useReduxState();
  const dispatch = useReduxDispatch();
  return (
    <div>
      {Math.random()}
      <div>
        <span>Count: {state.counter}</span>
        <button type="button" onClick={() => dispatch({ type: 'increment' })}>+1</button>
        <button type="button" onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      </div>
    </div>
  );
};

export default Counter;
Build In Progress

#Final notes

There could be some edge cases where this approach or the library doesn’t work well. I would love to hear your feedback either by Twitter, or GitHub issues.

The official react-redux also has a discussion about the Proxy approach. The possibility remains open that they might implement it in the future. Would be nice.

About Daishi

A freelance programmer. I'm interested in working remotely with people abroad.

Tokyo, Japan