Nishan Bende
Poorman's Observatory

Poorman's Observatory

Configuring useReducer with redux-devtools and thunk actions.

Configuring useReducer with redux-devtools and thunk actions.

Nishan Bende's photo
Nishan Bende
·May 30, 2019·

5 min read

  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.

  1. 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.

c0ab9126cbf263a0442b7a39417275d63c5dafca-712x205.png

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

71a9edc08921c380ddddee0502379a42a2cea6a3-1229x691.jpg

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

References

Article by Mihail Diordiev

Redux devtools documentation

 
Share this