Get state callback with useReducer in React - add reducer middlewares
Updated: I have published the snippets in this post to an npm package named enhanced-reducer.
Some of the killing features that IMHO useReducer
should support natively to convince react programmers to switch from using redux to useReducer
hook are
- Provide a convenient way to get the current state, i.e. implement
getState
function (my post will solve this) - Add middlewares (my post will solve this)
- Dispatch/getState from outside of the rendering flow (my post will not solve this)
In this post, I will introduce a hook called useEnhancedReducer
which solves 2 of the above 3 problems. The solution will guarantee the following conditions
- The
dispatch
function is constant on every render - The
getState
function is constant on every render
The implementation of useEnhancedReducer
which returns getState
is relatively simple
export const useEnhancedReducer = (reducer, initState, initializer) => {
const lastState = useRef(initState)
const getState = useCallback(() => lastState.current, [])
return [
...useReducer(
(state, action) => lastState.current = reducer(state, action),
initState,
initializer
),
getState
]
}
The trick is to create a ref keeping the last state of the reducer and update every time the reducer runs.
Full typescript version
export const useEnhancedReducer = (
reducer: Parameters<typeof useReducer>[0],
initState: Parameters<typeof useReducer>[1],
initializer: Parameters<typeof useReducer>[2]
) => {
const lastState = useRef<ReturnType<typeof reducer>>(initState)
const getState = useCallback(() => lastState.current, [])
return [
...useReducer(
(state: Parameters<typeof reducer>[0], action: Parameters<typeof reducer>[1]) => lastState.current = reducer(state, action),
initState,
initializer
),
getState
]
}
Codepen demo
See the Pen useEnhancedReducer sample by Tran Van Sang (@tranvansang) on CodePen.
Add custom middlewares.
Middlewares can be done in a similar manner by wrapping the dispatch function
const useEnhancedReducer = (reducer, initState, initializer, middlewares = []) => {
const lastState = useRef(initState)
const getState = useCallback(() => lastState.current, [])
const enhancedReducer = useRef((state, action) => lastState.current = reducer(
state,
action
)).current // to prevent reducer called twice, per: https://github.com/facebook/react/issues/16295
const [state, dispatch] = useReducer(
enhancedReducer,
initState,
initializer
)
const middlewaresRef = useRef(middlewares)
const enhancedDispatch = useMemo(()=>middlewaresRef.current.reduceRight(
(acc, mdw) => action => mdw(state)(getState)(acc)(action),
dispatch
), []) // value of middlewares is memoized in the first time of calling useEnhancedReducer(...)
return [state, enhancedDispatch, getState]
}
All middleware have the same signature and same as the following sample middleware.
const logMiddleware = state => getState => next => action => {
console.log('before action', action, getState())
next(action)
console.log('after action', action, getState()) // *NOTE*: because `dispatch(action)`` is not synchronous. it does not gurantee that this getState() call return the value after the action is applied.
}
Codepen demo
See the Pen useEnhancedReducer sample2 (middlewares) by Tran Van Sang (@tranvansang) on CodePen.