Configuring useReducer with redux-devtools and thunk actions.

Redux thunk like action creators for useReducer and redux devtools compatibility.

1. Creating a custom React useReducer hook to support passing functions as actions.

import { useReducer } from 'react'

function useReducerWithThunk(reducer, initialState) {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  let customDispatch = (action) => {
      if (typeof action === 'function') {
        action(customDispatch);
      } else {
        dispatch(action); 
      }
    };
  return [state, customDispatch];
}

If the type of action is a function instead of an object, we are calling it by passing it our own custom dispatch.

2. Using the above useReducer hook.

import useReducerWithThunk from "./useReducerWithThunk";

const thunk = () => async (dispatch) => {
  dispatch({ type:"FETCH" });
}

export default function App() {
  const [count, dispatch] = useReducerWithThunk(reducer, 0);
    
  function handleClick() {
    dispatch(thunk());
  }
    
  return <button onClick={handleClick}>Click me!</button>
}

Adding Redux devtools support

Before that, we need to consider the below points.

  • Devtools should support redux’s store along with React userReducer’s.
  • The useReducer is completely isolated and we can’t combine them to create a rootReducer which is what we are trying to avoid.
  • But, we can create a separate store for each useReducer in dev-tools and switch between them using the Select instance dropdown shown below.

Implementation

import { useReducer, useMemo, useEffect } from "react";

let stores = {};
let subscribers = {};

const REDUX_DEVTOOL_SET_STATE = "REDUX_DEVTOOL_SET_STATE";
const withDevTools = (name) => {
  return (
    name &&
    process.env.NODE_ENV === "development" &&
    typeof window !== "undefined" &&
    window.__REDUX_DEVTOOLS_EXTENSION__
  );
};

const devToolReducer = (reducer) => (state, action) => {
  if (action.type === REDUX_DEVTOOL_SET_STATE) {
    return action.state;
  } else {
    return reducer(state, action);
  }
};

function useReducerWithThunk(reducer, initialState, name) {
  let memoizedReducer = reducer;
  let shouldConfigDevTools = withDevTools(name);
  const nameWithUniqueNameSpace = getReducerName(name);

  // Memoizing to prevent recreation of devtoolReducer on each render.
  if (shouldConfigDevTools) {
    memoizedReducer = useMemo(() => devToolReducer(reducer), [reducer]);
  }

  const [state, dispatch] = useReducer(memoizedReducer, initialState);

  useEffect(() => {
    if (shouldConfigDevTools) {
      if (stores[name]) {
        throw new Error("More than one useReducerWithThunk have same name");
      }

      stores[nameWithUniqueNameSpace] = window.__REDUX_DEVTOOLS_EXTENSION__(
        reducer,
        initialState,
        {
          name: nameWithUniqueNameSpace,
        }
      );

      subscribers[nameWithUniqueNameSpace] = stores[
        nameWithUniqueNameSpace
      ].subscribe(() => {
        dispatch({
          type: REDUX_DEVTOOL_SET_STATE,
          state: stores[nameWithUniqueNameSpace].getState(),
        });
      });
    }

    return () => {
      if (shouldConfigDevTools) {
        subscribers[nameWithUniqueNameSpace]();
        subscribers[nameWithUniqueNameSpace] = undefined;
        stores[nameWithUniqueNameSpace] = undefined;
      }
    };
  }, []);

  const getState = () => state;

  const customDispatch = (action) => {
    if (typeof action === "function") {
      return action(customDispatch, getState);
    } else {
      if (shouldConfigDevTools && stores[nameWithUniqueNameSpace]) {
        stores[nameWithUniqueNameSpace].dispatch(action);
      } else {
        dispatch(action);
      }
    }
  };

  return [state, customDispatch];
}

const getReducerName = (name) => {
  return "userReducerThunk_" + name;
};

export default useReducerWithThunk;

Usage

The above useReducer — React redux hook can be used as below.

const [count, dispatch] = useReducerWithThunk(reducer, 0, name // optional);

Result

End Notes

It would be much better if only a few complicated reducers are connected to Redux devtools or else the overhead involved in switching and finding the appropriate instance will exceed the benefits.

But at least the reason for not able to use dev tools should not prevent you from keeping the state local.

Source

npm

Implementation references

Article by Mihail Diordiev

Redux devtools documentation