Last active
February 5, 2022 18:22
-
-
Save sebringj/9b6d259fffa3744761de2a9618f7d980 to your computer and use it in GitHub Desktop.
DumbContextFactory: Enables easy React Context creation with optional browser storage or custom storage options
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { FC } from "react"; | |
import { UserContextProvider, useUserContext } from "./UserContext"; | |
const Component1: FC = () => { | |
const { user } = useUserContext(); | |
return user.loaded ? ( | |
<h1> | |
Hello {user.firstName} {user.lastName} | |
</h1> | |
) : ( | |
<div>loading...</div> | |
); | |
}; | |
const Component2: FC = () => { | |
const { user, setUser } = useUserContext(); | |
return ( | |
<div> | |
<input | |
placeholder="first name" | |
value={user.firstName} | |
onChange={(ev) => setUser({ firstName: ev.target.value })} | |
/> | |
<input | |
placeholder="last name" | |
value={user.lastName} | |
onChange={(ev) => setUser({ lastName: ev.target.value })} | |
/> | |
</div> | |
); | |
}; | |
export default function App() { | |
return ( | |
<UserContextProvider> | |
<div className="App"> | |
<Component1 /> | |
<Component2 /> | |
</div> | |
</UserContextProvider> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, FC, useContext, useEffect } from "react"; | |
export interface IContext<T> { | |
state: T; | |
setState: (state: T) => void; | |
} | |
export type CustomStorage<T> = { | |
save: (state: T) => Promise<void>; | |
hydrate: () => Promise<T>; | |
}; | |
export type BrowserStorage = { | |
type: "session" | "local"; | |
key: string; | |
}; | |
type Options<T> = { | |
persist: BrowserStorage | CustomStorage<T> | "none"; | |
}; | |
type ContextState<T> = { | |
state: T; | |
setState: (state: T) => void; | |
}; | |
export default function DumbContextFactory<T>( | |
nameOfContext: string, | |
defaultState: T, | |
options?: Options<T> | |
) { | |
function getDefaultState() { | |
return JSON.parse(JSON.stringify(defaultState)) as T; | |
} | |
function getHydrate(): () => Promise<T> { | |
if (!options || options?.persist === "none") { | |
return () => Promise.resolve(getDefaultState()); | |
} | |
const browserStorage = options.persist as BrowserStorage; | |
if (browserStorage.type) { | |
const storage = window[`${browserStorage.type}Storage`]; | |
return () => { | |
let state: T; | |
try { | |
state = JSON.parse(storage.getItem(browserStorage.key) || "") as T; | |
} catch (err) { | |
state = getDefaultState(); | |
} | |
return Promise.resolve({ ...getDefaultState(), ...state }); | |
}; | |
} | |
const customStorage = options.persist as CustomStorage<T>; | |
return customStorage.hydrate; | |
} | |
const hydrate = getHydrate(); | |
function getSave(): (state: T) => Promise<void> { | |
if (!options || options?.persist === "none") { | |
return (_state: T) => Promise.resolve(); | |
} | |
const browserStorage = options.persist as BrowserStorage; | |
if (browserStorage.type) { | |
const storage = window[`${browserStorage.type}Storage`]; | |
return (state: T) => { | |
storage.setItem(browserStorage.key, JSON.stringify(state)); | |
return Promise.resolve(); | |
}; | |
} | |
const customStorage = options.persist as CustomStorage<T>; | |
return customStorage.save; | |
} | |
const save = getSave(); | |
const DumbContext = React.createContext<ContextState<T>>({ | |
state: getDefaultState(), | |
setState: (_state: T) => {} | |
}); | |
const DumbContextProvider: FC = ({ children }) => { | |
const [state, setState] = useState(getDefaultState()); | |
useEffect(() => { | |
async function callHydrate() { | |
const hydrated = await hydrate(); | |
setState(hydrated); | |
} | |
callHydrate(); | |
}, []); | |
useEffect( | |
function onStateChange() { | |
save(state); | |
}, | |
[state] | |
); | |
return ( | |
<DumbContext.Provider value={{ state, setState }}> | |
{children} | |
</DumbContext.Provider> | |
); | |
}; | |
const capitalizedName = | |
nameOfContext.charAt(0).toUpperCase() + nameOfContext.slice(1); | |
const useDumbContext = () => { | |
const context = useContext(DumbContext); | |
if (!context) { | |
throw new Error( | |
`use${capitalizedName} must be used in a component wrapped with ${capitalizedName}Provider` | |
); | |
} | |
return context; | |
}; | |
return { | |
DumbContextProvider, | |
useDumbContext | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import DumbContextFactory from "./DumbContextFactory"; | |
export type UserState = { | |
loaded: boolean; | |
firstName: string; | |
lastName: string; | |
}; | |
const defaultState: UserState = { | |
loaded: true, | |
firstName: "", | |
lastName: "" | |
}; | |
const { DumbContextProvider, useDumbContext } = DumbContextFactory<UserState>( | |
"UserContext", | |
defaultState, | |
{ | |
persist: { | |
type: "local", | |
key: "user" | |
} | |
} | |
); | |
const useUserContext = () => { | |
const { state, setState } = useDumbContext(); | |
return { | |
user: state, | |
setUser: (newState: Partial<UserState>) => { | |
setState({ ...state, ...newState }); | |
} | |
}; | |
}; | |
export { DumbContextProvider as UserContextProvider, useUserContext }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment