Skip to content

Instantly share code, notes, and snippets.

@okapies
Last active June 4, 2020 16:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save okapies/67a3f1daa09d70806c4e70cdaeaea549 to your computer and use it in GitHub Desktop.
Save okapies/67a3f1daa09d70806c4e70cdaeaea549 to your computer and use it in GitHub Desktop.
A custom React Context persisting its state to localStorage
// The original idea comes from a post by Alex Krush.
// https://medium.com/@akrush95/global-cached-state-in-react-using-hooks-context-and-local-storage-166eacf8ab46
import React, { useCallback, useEffect, useReducer, useRef } from 'react';
export const createCachedContext = ({ storageKey, defaultValue }) => {
const initializer = (initialState) => {
const localState = localStorage.getItem(storageKey);
if (localState) {
try {
return JSON.parse(localState);
} catch {
// localState is not a object, so return the raw string
return localState;
}
} else if (typeof initialState === 'string' || typeof initialState === 'object') {
return initialState;
} else {
throw new Error('defaultValue must be object or string');
}
};
const reducer = (value, newValue) => {
const newValueType = typeof newValue;
if (newValueType === 'undefined' || newValue === null) {
return defaultValue;
} else if (typeof value === newValueType) {
if (newValueType === 'string') {
return newValue;
} else if (newValueType === 'object') {
return { ...value, ...newValue };
}
}
throw new Error(`${newValueType} value cannot be set to the context`);
};
const Context = React.createContext();
const Provider = (props) => {
const [value, setValue] = useReducer(reducer, defaultValue, initializer);
const initialized = useRef(false);
// save the updated value as a side-effect
useEffect(() => {
if (initialized.current) {
const valueType = typeof value;
if (valueType === 'string') {
localStorage.setItem(storageKey, value);
} else if (valueType === 'object') {
localStorage.setItem(storageKey, JSON.stringify(value));
} else {
// I believe reducer will prevent from reaching this line...
throw new Error('value must be object or string');
}
} else {
// skip saving for the first time
initialized.current = true;
}
}, [value]);
// re-initialize when the storage has been modified by another window
const handleStorageEvent = useCallback((e) => {
if (e.key === null || e.key === storageKey) {
// do not set `initialized` to false because I'm not sure it is safe
setValue(initializer(defaultValue));
}
}, []);
useEffect(() => {
if (typeof window !== 'undefined') {
window.addEventListener('storage', handleStorageEvent);
return () => {
window.removeEventListener('storage', handleStorageEvent);
};
}
}, []);
return (
<Context.Provider value={[value, setValue]}>
{props.children}
</Context.Provider>
);
};
return [Context, Provider];
}
import React, { useEffect, useState } from "react";
import { createCachedContext } from "./CachedContext";
export const [ProfileContext, ProfileProvider] = createCachedContext({
storageKey: 'profile',
defaultValue: {
name: null,
email: null,
}
});
const App = (props) => {
return <ProfileProvider><Hello /></ProfileProvider>;
}
const Hello = (props) => {
const [name, setName] = useState(null);
const [profile, setProfile] = useContext(ProfileContext);
useEffect(() => {
setProfile({name: name});
}, [name]);
}
@okapies
Copy link
Author

okapies commented Jun 4, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment