From b5e542cc2e5a2d0ce3618f9e97b03ee946b1f962 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:26:43 -0800 Subject: [PATCH] chore: createPersistedStateFunction --- .../persistedState/createPersistedState.tsx | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/renderer/src/contexts/persistedState/createPersistedState.tsx diff --git a/src/renderer/src/contexts/persistedState/createPersistedState.tsx b/src/renderer/src/contexts/persistedState/createPersistedState.tsx new file mode 100644 index 0000000..583f1d7 --- /dev/null +++ b/src/renderer/src/contexts/persistedState/createPersistedState.tsx @@ -0,0 +1,128 @@ +import { createContext, useContext, type ReactNode } from 'react' +import { + createStore as createZustandStore, + useStore, + type StateCreator, + type StoreApi, +} from 'zustand' +import { persist } from 'zustand/middleware' + +type PersistedStoreKey = 'ActiveProjectId' + +/** + * Follows the pattern of injecting persisted state with a context. See + * https://tkdodo.eu/blog/zustand-and-react-context. Allows for easier testing + */ +export function createPersistedStoreWithProvider({ + slice, + actions, + persistedStoreKey, +}: { + slice: T + actions: StoreActions + persistedStoreKey: PersistedStoreKey +}) { + const Context = createContext> | null>(null) + + const Provider = ({ + children, + store, + }: { + children: ReactNode + store: ReturnType> + }) => { + return {children} + } + + function useCurrentContext() { + const context = useContext(Context) + if (!context) { + throw new Error(`${persistedStoreKey} context not properly initialized`) + } + return context + } + + // Hook to select store state + function useCurrentStore(selector: (state: T) => Selected) { + const context = useCurrentContext() + return useStore(context.store as StoreApi, selector) + } + + function useActions() { + const context = useCurrentContext() + return context.actions + } + + return { + Provider, + createStore: ({ isPersisted }: { isPersisted: boolean }) => { + return isPersisted + ? createStore({ isPersisted: true, persistedStoreKey, actions, slice }) + : createStore({ actions, slice, isPersisted: false }) + }, + useCurrentStore, + useActions, + } +} + +function createPersistedStore( + ...args: Parameters> +) { + const store = createZustandStore()(createPersistMiddleware(...args)) + store.setState((state) => ({ + ...state, + ...args[0], + })) + + return store +} + +function createPersistMiddleware( + slice: StateCreator, + persistedStoreKey: PersistedStoreKey, +) { + return persist(slice, { + name: persistedStoreKey, + }) +} + +type ActionCreator = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + newState: any, +) => (set: StoreApi['setState'], get: StoreApi['getState']) => void + +type StoreActions = { [key: string]: ActionCreator } + +type createStoreProps = { + slice: T + actions: StoreActions +} & ( + | { isPersisted: false } + | { isPersisted: true; persistedStoreKey: PersistedStoreKey } +) + +export function createStore(props: createStoreProps) { + let store: StoreApi + + if (!props.isPersisted) { + store = createZustandStore()(() => ({ ...props.slice })) + } else { + store = createPersistedStore( + () => ({ ...props.slice }), + props.persistedStoreKey, + ) + } + + const actions = Object.fromEntries( + Object.entries(props.actions).map(([key, action]) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const wrappedAction = (newState: any) => { + return action(newState)(store.setState, store.getState) // Pass `setState` and `getState` + } + + return [key, wrappedAction] + }), + ) as StoreActions + + return { store, actions } +}