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.