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