Some of 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 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 above 3 problems. The solution will guarantee 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>[1]
) => {
  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
  ]
}

Add custom middlewares.

Middlewares can be done in a similar manner by wrapping the dispatch function

export const useEnhancedReducer = (reducer, initState, initializer, middlewares = []) => {
  const lastState = useRef(initState)
  const getState = useCallback(() => lastState.current, [])
  const [state, dispatch] = useReducer(
      (state, action) => lastState.current = reducer(
        state,
        action
      ),
      initState,
      initializer
    )
  const enhancedDispatchRef = useRef(middleewares.reduceRight(
    (acc, mdw) => action => mdw(state)(getState)(acc)(action),
    dispatch
  ))
  return [state, enhancedDispatchRef.current, getState]
}

All middleware have a same signature and same as following sample middleware.

const logMiddleware = state => getState => next => action => {
  console.log('before action', action)
  next(action)
  console.log('after action', action)
}