Hi, my name is Sergey. I create video-tutorials, talk at meetups, experiment with different technologies and tweet stuff.

Building useReducer with logger

If you've played with useReducer hook and are coming from Redux background, you are probably missing some of useful middleware like logging. In this post we are going to build a custom useReducerWithLogger hook.

This is our starting point.

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
}

The first thing we need to do is to create a custom hook and use it instead of useReducer. Right now it does absolutely nothing though.

function useReducerWithLogger(reducer, initialState) {  return useReducer(reducer, initialState);}
function App() {
  const [state, dispatch] = useReducerWithLogger(reducer, initialState);
}

Did you know that useReducer accepts a third optional parameter? We forgot it. To save us from potential headache we can use spread syntax to accept and pass any number of arguments.

function useReducerWithLogger(...args) {  return useReducer(...args);}
function App() {
  const [state, dispatch] = useReducerWithLogger(reducer, initialState);
}

Now we can use the power of useEffect to log state every time it changes.

function useReducerWithLogger(...args) {
  const [state, dispatch] = useReducer(...args);

  useEffect(() => {    console.log('Next state', state);  }, [state]);
  return [state, dispatch];
}

What about previous state? If we open handy React documentation we will find the answer to our question: useRef. The object it returns won't be recreated after every render. It can be used similarly to class instance properties.

function useReducerWithLogger(...args) {
  let prevState = useRef(initialState);  const [state, dispatch] = useReducer(...args);

  useEffect(() => {
    console.log('Prev state: ', prevState.current);
    console.log('Next state: ', state);
    prevState.current = state;  }, [state]);

  return [state, dispatch];
}

We would also like to display the type of dispatched action. We are going to borrow a concept from functional programming called higher order functions and replace our dispatch function with a custom one.

function withLogger(dispatch) {  return function (action) {    console.log('Action Type:', action.type);    return dispatch(action);  }}
function useReducerWithLogger(...args) {
  let prevState = useRef(initialState);
  const [state, dispatch] = useReducer(...args);
  
  const dispatchWithLogger = withLogger(dispatch);
  useEffect(() => {
    console.log('Prev state: ', prevState.current);
    console.log('Next state: ', state);
    prevState.current = state;
  }, [state]);

  return [state, dispatchWithLogger];}

If we don't want the function to be recreated on every render we can also memoize it.

const dispatchWithLogger = useMemo(() => {
  return withLogger(dispatch);
}, [dispatch]);

As a final touch we can use console.groupCollapsed to keep our console output clean and readable.

function enchanceDispatchWithLogger(dispatch) {
  return function (action) {
    console.groupCollapsed('Action Type:', action.type);    return dispatch(action);
  }
}

function useReducerWithLogger(...args) {
  let prevState = useRef(initialState);
  const [state, dispatch] = useReducer(...args);

  const dispatchWithLogger = useMemo(() => {
    return enchanceDispatchWithLogger(dispatch);
  }, [dispatch]);

  useEffect(() => {
    if (state !== initialState) {
      console.log('Prev state: ', prevState.current);
      console.log('Next state: ', state);
      console.groupEnd();    }
    prevState.current = state;
  }, [state]);


  return [state, dispatchWithLogger];
}

The final version is available on codesandbox, feel free to play with it and use it in your projects.