|
/** |
|
* this is a automated and typeful version of https://github.com/pmndrs/zustand/blob/bf9d0922fe9292c2399b9594eafe9304016b52d9/docs/guides/initialize-state-with-props.md |
|
* I'm not sure if it is a great approach of doing it. because I don't have much knowledge of how SSR codes should work. |
|
* That's why I placed it in gist, not a repository. if you have any idea of improving it or if you think there are better ways, I'd so glad to know :) |
|
*/ |
|
|
|
import { StateCreator, StoreMutatorIdentifier, createStore, useStore } from "zustand" |
|
import { Context, createContext, useContext, useRef } from "react" |
|
|
|
type StoreHydrateData = { |
|
hydrate: { |
|
handler: (data: any) => void; |
|
name: string; |
|
} |
|
} |
|
|
|
type StoreCreators = Record<string, ReturnType<typeof makeStoreCreator>> |
|
|
|
type Stores<TStores extends StoreCreators> = { |
|
[key in keyof TStores]?: Parameters<ReturnType<ReturnType<TStores[key]['createStore']>['getState']>['hydrate']['handler']>[0] |
|
} |
|
|
|
type PropsType<TZustandProps extends Record<string, unknown>, TAppProps extends Record<string, unknown> = {}> = TAppProps & { |
|
zustand: TZustandProps |
|
} |
|
|
|
type Contexts<TStores extends StoreCreators> = { [key in keyof TStores]: Context<ReturnType<TStores[key]['createStore']> | null> } |
|
|
|
type Hooks<TStores extends StoreCreators> = { |
|
[key in keyof TStores extends string ? `use${Capitalize<keyof TStores>}Store` : never]: UseSelect<TStores[key extends `use${infer Name}Store` ? Uncapitalize<Name> : never]['Types']['State']> |
|
} |
|
|
|
type UseSelect<TState> = <T extends unknown>( |
|
selector: (state: TState) => T, |
|
equalityFn?: (left: T, right: T) => boolean |
|
) => T |
|
|
|
export class NextZustandWrapper<TStores extends StoreCreators> { |
|
/** |
|
* contexts of stores with default value null |
|
*/ |
|
contexts = Object.fromEntries(Object.entries(this.stores).map(([name]) => { |
|
return [name, createContext<Stores<TStores>[typeof name] | null>(null)] |
|
})) as Contexts<TStores> |
|
|
|
/** |
|
* Provider that handles "context.Provider"s and initial values |
|
*/ |
|
Provider = createProvider(this.stores, this.contexts) |
|
|
|
/** |
|
* create a typed instance of NextZustandWrapper |
|
*/ |
|
static create<TStores extends StoreCreators>(stores: TStores) { |
|
return new NextZustandWrapper(stores) as NextZustandWrapper<TStores> & Hooks<TStores> |
|
} |
|
|
|
private constructor(private stores: TStores) { |
|
for (const name in this.stores) { |
|
// assign hooks to instance |
|
|
|
const useHook: UseSelect<TStores[typeof name]['Types']['State']> = ( |
|
selector, |
|
equalityFn |
|
) => { |
|
const store = useContext(this.contexts[name]) |
|
if (!store) throw new Error(`Missing ${name}.Provider in the tree. looks like the initialData for this store is not entered`) |
|
|
|
return useStore(store, selector, equalityFn) |
|
} |
|
|
|
Object.assign(this, { |
|
[`use${name[0].toUpperCase()}${name.slice(1)}Store`]: useHook |
|
}) |
|
} |
|
} |
|
|
|
/** |
|
* just a helper method to merge normal page props and zustand initial data in GetStaticProps or GetServerSideProps |
|
*/ |
|
withProps(zustandProps: Stores<TStores>): PropsType<Stores<TStores>>; |
|
withProps<TProps extends Record<string, unknown>>(props: TProps, zustandProps: Stores<TStores>): PropsType<Stores<TStores>, TProps>; |
|
withProps(props: Record<string, unknown>, zustandProps?: Record<string, unknown>): PropsType<Record<string, unknown>> { |
|
return { |
|
zustand: zustandProps || props, |
|
...(zustandProps && props) |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* returns object which includes a creator function (createStore) and types |
|
*/ |
|
export const makeStoreCreator = <T extends StoreHydrateData, Mos extends [StoreMutatorIdentifier, unknown][] = []>(initializer: StateCreator<T, [], Mos>) => { |
|
return { |
|
createStore: () => createStore(initializer), |
|
Types: { |
|
State: undefined as unknown as T |
|
} |
|
} |
|
} |
|
|
|
type WrapperProviderProps<T extends Record<string, unknown>> = { |
|
children?: React.ReactNode; |
|
initialData?: PropsType<T> | T; |
|
} |
|
|
|
export const createProvider = <TStoreCreators extends StoreCreators, TContexts extends Contexts<TStoreCreators>>(storeCreators: TStoreCreators, contexts: TContexts) => { |
|
const Provider: React.FC<WrapperProviderProps<Stores<TStoreCreators>>> = ({ children, initialData }) => { |
|
const storesRef = useRef<{ |
|
[key in keyof TStoreCreators]: { |
|
store: ReturnType<TStoreCreators[key]['createStore']>; |
|
latestData: TStoreCreators[key]['Types']['State']; |
|
} |
|
}>({} as never) |
|
|
|
for (const name in storeCreators) { |
|
const data = initialData?.zustand?.[name] || initialData?.[name as keyof typeof initialData] |
|
if (data && JSON.stringify(data) !== JSON.stringify(storesRef.current?.[name]?.latestData)) { |
|
const store = storeCreators[name].createStore() |
|
store.getState().hydrate.handler(data) |
|
|
|
storesRef.current[name] = { |
|
store: store as never, |
|
latestData: data as never |
|
} |
|
} |
|
} |
|
|
|
return <> |
|
{Object.entries(initialData?.zustand || initialData || {}).reduce((children, [name, data]) => { |
|
const ContextProvider = contexts[name].Provider |
|
|
|
return ( |
|
<ContextProvider value={storesRef.current[name].store} > |
|
{children} |
|
</ContextProvider > |
|
) |
|
}, children)} |
|
</> |
|
} |
|
|
|
return Provider |
|
} |