Skip to content

Instantly share code, notes, and snippets.

@arashi-dev
Last active December 30, 2022 16:45
Show Gist options
  • Save arashi-dev/a8d01d206f4c315faef5c888d33f03df to your computer and use it in GitHub Desktop.
Save arashi-dev/a8d01d206f4c315faef5c888d33f03df to your computer and use it in GitHub Desktop.

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 :)

bear.store.ts
export const createBearStore = makeStoreCreator((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
  
  zustand: {
      handler: bears => set({ bears }),
      name: "bear" // not really necessary. we can remove it completely
  }
}))
wrapper.ts
const wrapper = NextZustandWrapper.create({
    // the name "bear" can also be automated. we can make this object like so: `{ createBearStore }`. then, extract the "Bear" from "createBearStore" and make it Uncapitalized. it will still be typeful
    bear: createBearStore
})
_App.tsx
export default const _App => ({Component, pageProps}) => {
    return (
        <wrapper.Provider initialData={pageProps}>
            <Component {...pageProps} />
        </wrapper.Provider>
    )
}
index.tsx (homepage)
export const getStaticProps = () => {
    return {
        props: wrapper.withProps(normalPageProps, {
            bear: 100
        })
    }
}
read data in component
const MyComp = () => {
    const bears = wrapper.useBearStore(state => state.bears)
}
/**
* 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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment