The built-in useReducer
hook is a great tool for managing multiple pieces of component state that need to be updated together. In a recent use case, I was managing a user's preferances/settings withing an app with useReducer
and needed to sync those settings with local storage.
On the component level, the state was initially managed as follows:
// reducer
const calculationParamsReducer = (state: UserCustomisableParams, action: ActionType) => {
switch (action.type) {
case 'updatePreference1':
return { ...state, preferance1: action.payload };
case 'updatePreference2':
return { ...state, preferance2: action.payload };
}
};
// initial state
const defaultUserPreferences = {
preferance1: true,
preference2: false,
};
// usage
const [userCustomisedParams, dispatch] = useReducer(calculationParamsReducer, defaultUserPreferences);
I'd been using a custom hook to sync component state to local storage with a variant of the useLocalStorage hook. Its API looks like this:
// the hook takes in a key and a default value
const [name, setName] = useLocalStorage<string>('name', 'Bob');
The goal was to compose a new custom hook from this useLocalStorage
hook that worked with useReducer
.
Composing useLocalStorage with useReducer
Fortunately, I stumbled upon useReducerWithLocalStorage by Mattia Richetto, which shows how to compose useLocalStorage with useReducer
.
The hook was a fit for my use case except that it wasn't written in TypeScript. I modified it slightly by making the passing of arguments resemble useReducer
and rewrote it in TypeScript:
import * as React from 'react';
// useLocalStorage from useHooks
import { useLocalStorage } from './useLocalStorage';
export const useReducerWithLocalStorage = <S, A>(reducer: React.Reducer<S, A>, initializerArg: S, key: string) => {
const [localStorageState, setLocalStorageState] = useLocalStorage(key, initializerArg);
return React.useReducer(
(state: S, action: A) => {
const newState = reducer(state, action);
setLocalStorageState(newState);
return newState;
},
{ ...localStorageState }
);
};
export default useReducerWithLocalStorage;
Now I had a flexible hook that provided me with the required type information.
Applying the hook to the problem
I could now take this new hook to use to manage user preferances in the app like so:
// reducer
const calculationParamsReducer = (
state: UserCustomisableParams,
action: ActionType,
) => {
switch (action.type) {
case 'updatePreference1':
return { ...state, preferance1: action.payload }
case 'updatePreference2':
return { ...state, preferance2: action.payload }
}
}
// initial state
const defaultUserPreferences = {
preferance1: true,
preference2: false,
}
// usage
const [userCustomisedParams, dispatch] =
useReducerWithLocalStorage(
calculationParamsReducer,
defaultUserPreferences,
'user-preferences',
)
Here's a link to the a gist of this hook.