-
-
Save mattiaerre/8dbd2d8efca3f242c7085a9ce82ecbde to your computer and use it in GitHub Desktop.
import { useState } from 'react'; | |
// credit: https://usehooks.com/useLocalStorage/ | |
function useLocalStorage(key, initialValue) { | |
const [storedValue, setStoredValue] = useState(() => { | |
try { | |
const item = window.localStorage.getItem(key); | |
return item ? JSON.parse(item) : initialValue; | |
} catch (error) { | |
console.log(error); | |
return initialValue; | |
} | |
}); | |
function setValue(value) { | |
try { | |
const valueToStore = | |
value instanceof Function ? value(storedValue) : value; | |
setStoredValue(valueToStore); | |
window.localStorage.setItem(key, JSON.stringify(valueToStore)); | |
} catch (error) { | |
console.log(error); | |
} | |
} | |
return [storedValue, setValue]; | |
} | |
export default useLocalStorage; |
import { useReducer } from 'react'; | |
import useLocalStorage from './useLocalStorage'; | |
function useReducerWithLocalStorage({ initializerArg, key, reducer }) { | |
const [localStorageState, setLocalStorageState] = useLocalStorage( | |
key, | |
initializerArg | |
); | |
return useReducer( | |
(state, action) => { | |
const newState = reducer(state, action); | |
setLocalStorageState(newState); | |
return newState; | |
}, | |
{ ...localStorageState } | |
); | |
} | |
export default useReducerWithLocalStorage; |
import { renderHook, act } from '@testing-library/react-hooks'; | |
import useLocalStorage from './useLocalStorage'; | |
import useReducerWithLocalStorage from './useReducerWithLocalStorage'; | |
jest.mock('./useLocalStorage'); | |
const emptyState = { firstName: '', lastName: '' }; | |
const mockSetLocalStorageState = jest.fn(); | |
const mockLocalStorageState = { ...emptyState, lastName: 'Doe' }; | |
useLocalStorage.mockImplementation(() => [ | |
mockLocalStorageState, | |
mockSetLocalStorageState | |
]); | |
const REACT_APP_STATE = 'REACT_APP_STATE'; | |
const CHANGE_FIRST_NAME = 'CHANGE_FIRST_NAME'; | |
function reducer(state, action) { | |
switch (action.type) { | |
case CHANGE_FIRST_NAME: | |
return { ...state, firstName: action.value }; | |
default: | |
return state; | |
} | |
} | |
const mockReducer = jest.fn((state, action) => reducer(state, action)); | |
test('useReducerWithLocalStorage', () => { | |
const { | |
result: { | |
current: [state, dispatch] | |
} | |
} = renderHook(() => | |
useReducerWithLocalStorage({ | |
initializerArg: emptyState, | |
key: REACT_APP_STATE, | |
reducer: mockReducer | |
}) | |
); | |
expect(useLocalStorage).toBeCalledWith(REACT_APP_STATE, emptyState); | |
expect(state).toEqual(mockLocalStorageState); | |
act(() => { | |
dispatch({ type: CHANGE_FIRST_NAME, value: 'John' }); | |
}); | |
expect(mockSetLocalStorageState).toBeCalledWith({ | |
firstName: 'John', | |
lastName: 'Doe' | |
}); | |
}); |
this is awesome @mdboop thanks for that. You are absolutely right. I like very much your suggestion and
I think you would have to introduce a new argument, something like defaultOverrides or something, because you couldn't simply merge the states (you would end up writing over every key).
that or
One other option is to add an additional feature to this reducer to pass a list of properties I do not wish to store locally
in this case, the defaultOverrides
will be inferred by the initialState
.
also, I've created a repo for this component as well as published it to npm
:
-
https://github.com/Tweries/useReducerWithLocalStorage heavily WIP
-
https://www.npmjs.com/package/use-reducer-with-local-storage
-
https://github.com/Tweries/silver-tip just a playground where I use this hook
how about this?
import { useReducer } from 'react';
import remove from './remove';
import useLocalStorage from './useLocalStorage';
function useReducerWithLocalStorage({
blacklist = [],
initializerArg,
key,
reducer
}) {
const [localStorageState, setLocalStorageState] = useLocalStorage(
key,
remove({ blacklist, state: initializerArg })
);
return useReducer(
(state, action) => {
const newState = reducer(state, action);
setLocalStorageState(remove({ blacklist, state: newState }));
return newState;
},
{ ...initializerArg, ...localStorageState }
);
}
export default useReducerWithLocalStorage;
Ah, maybe I didn't put it quite right, but I'm thinking of the case where you have local storage state AND you have temporary state initialized by the component. Take this (bad) example:
In this case, when you refresh the page, you end up with status showing as
ready
even though what I would want here isloading
along with the text input saved in localstorage. Maybe this is a use case you don't want to support, and I would totally respect that, but I could see it being useful.I think you would have to introduce a new argument, something like
defaultOverrides
or something, because you couldn't simply merge the states (you would end up writing over every key).And yes, just re-reading this:
For sure, I think I'm taking something that is clean and elegant and jamming too many features into it. It's probably better to keep them separate as you're suggesting. I guess I'm still just thinking of cases where you're using a reducer to coordinate related state and some of it is transient and some is persisted, but yeah... this makes it more complicated. Feature creep! Anyway, this was fun to take a look at. Excellent little hook here.
edit: added
disabled
state to loading state to show how they might be linked.