import * as React from "react"; import type { StoreApi } from "zustand"; import { devtools } from "zustand/middleware"; import { UseBoundStoreWithEqualityFn, createWithEqualityFn as create, } from "zustand/traditional"; import { useSuffixContext } from "../contexts"; import { shallow } from "../utils"; import { omitCommonProps } from "../utils/props"; export type State = Record> & { reset: (id: string) => void; set: ( id: string, newStateForId: Record, name?: string, ) => void; }; export const ZustandContext = React.createContext< UseBoundStoreWithEqualityFn> >({} as any); export function StoreProvider({ store, children }) { return ( {children} ); } type StateSelector = (state: State) => StateSlice; type EqualityChecker = (a: StateSlice, b: StateSlice) => boolean; export function useStore( selector: StateSelector, equalityFn?: EqualityChecker, ) { const useZStore = React.useContext(ZustandContext); return useZStore(selector, equalityFn); } export const createDebugStore = ( initialValues: Record> = {}, ) => create( devtools( (set) => ({ ...initialValues, reset: (id) => { set( (oldState: State) => { const { [id]: _, ...rest } = oldState; return rest; }, true, `reset ${id}`, ); }, set: ( id: string, newStateForId: Record, name?: string, ) => { set( (oldState: State) => { const mergedDataForId = Object.assign( {}, oldState[id], typeof newStateForId === "function" ? (newStateForId as any)(oldState[id]) : newStateForId, ); return Object.assign({}, oldState, { [id]: mergedDataForId, }); }, false, name, ); }, }) as State, { name: "BrevityStore" }, ), shallow, ); export const createStore = ( initialValues: Record> = {}, ) => create( (set) => ({ ...initialValues, reset: (id) => { set((oldState: State) => { const { [id]: _, ...rest } = oldState; return rest; }, true); }, set: ( id: string, newStateForId: Record, name?: string, ) => { set((oldState: State) => { const mergedDataForId = Object.assign( {}, oldState[id], typeof newStateForId === "function" ? (newStateForId as any)(oldState[id]) : newStateForId, ); return Object.assign({}, oldState, { [id]: mergedDataForId, }); }, false); }, }) as State, shallow, ); const DEFAULT_STATE = {}; export const useGet = ( ids: string[], getKey = (id: string) => id, suffix?: string, ) => { const selector = (state: State) => { return ids.reduce( (acc, id) => { // I'm not sure if we actually need the suffix piece. This is here to preserve old haviour which was erronious I think but I'm scared I'll break an app // relying this this behviour so I'm leaving it in. acc[id] = state[id] ?? state[getKey(id)] ?? (suffix ? state[`${id}-${suffix}`] : DEFAULT_STATE) ?? DEFAULT_STATE; return acc; }, {} as Record, ); }; return useStore(selector, (a, b) => shallow(a, b, false)) as Record< string, unknown >; }; export function useGetSet>( id: string, data: T = DEFAULT_STATE as T, disableReset = false, ): [T, (value: Partial, name?: string) => void, () => void] { const initialData = React.useRef(data); const selector = React.useCallback( (state: State) => { const setter = state.set; const reset = state.reset; if (id in state) { return [state[id], setter, reset, true]; } const newState = Object.assign({}, data); return [newState, setter, reset, false] as const; }, [id, data], ); const [value, set, reset, found] = useStore(selector, shallow) as any; const resetLocal = React.useCallback(() => { reset(id); }, [id, reset]); React.useEffect(() => { if (!found) { set( id, data, process.env.PREVIEW ? `initialize: ${id.slice(-4)}` : undefined, ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [found, id]); React.useEffect(() => { if (!shallow(data, initialData.current, true)) { initialData.current = data; set( id, data, process.env.PREVIEW ? `update: ${id.slice(-4)}` : undefined, ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]); React.useEffect(() => { if (disableReset) { return; } return resetLocal; }, [resetLocal, disableReset]); const setter = React.useCallback( (newStateForId: Record, name?: string) => { set( id, newStateForId, process.env.PREVIEW ? `${name} (${id.slice(-4)})` : undefined, ); }, // eslint-disable-next-line react-hooks/exhaustive-deps [id], ); return [value as T, setter, resetLocal]; } export const StateProvider = ({ children, ids, className, getKey = (id) => id, ...props }: { ids: string[]; getKey?: (id: string) => string; children: (state?: Record) => React.ReactNode; className?: string; ref?: React.Ref; }) => { const suffix = useSuffixContext(); const kids = children(useGet(ids, getKey, suffix)); return ( <> {React.Children.map(kids, (child) => React.cloneElement(child as any, omitCommonProps(props)), )} ); };