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.