In its simplest form, zustand is easily scaffolded:
This code implements a vanilla store that can be used within any framework. No store caching is used here.
import { createStore, StoreApi } from "zustand";
export interface MyStoreState {};
export interface MyStoreAPI {};
export type MyStoreViewModel = MyStoreState & { api: MyStoreAPI };
/**
* Factory function; called 1x
* Usually NOT exported
*/
export const buildMyStore = (/** injected services here */) => {
const configureStore = (
set: (state: any) => any,
get: () => MyStoreState,
store: StoreApi<MyStoreViewModel>,
): MyStoreViewModel => {
const state = { /** initialized state here */ };
const api = { /** custom api to publish here */ };
return {...state, api}
};
return createStore<MyStoreViewModel>()(configureStore);
};
Notice this store above does NOT include the middleware (
devTools
,immer
,persist
,bookmark
,compute
). These provide powerful store features when needed.
With React, we employ the useStore() to subscribed to store mutations and trigger UI re-renders.
type Result = StoreAPI<MyStoreViewModel>; // only used to clarify code below
export const useMyStore = createStore<Result>(buildMyStore);
Here ^, we build a Zustand React hook as a const
.
This creates a singleton hook which caches the store instance!
This caching is important to ensure that the store is not recreated on every render and can be shared across components.
This is using ES6 modules to cache and publish the
const useMyStore
instance.
Unfortunately, the above code does NOT easily allow the store to use authenticated, remote dataservices.
If buildMyStore()
needs dataservices (for remote CRUD), then we should use create()
instead of createStore()
:
/**
* This creates a singleton store instance and uses `useStore` to subscribe/publish it.
* !! Not exported because it is ONLY used by useDocumentsSidebar()
*/
let storeInstance: any = null;
export const useDocumentsStore = (service: DocumentsDataService, t: TranslateFn) => {
if (!storeInstance) {
storeInstance = create<StoreApi<DocumentsViewModel>>(buildDocumentsStore(service, t));
}
return useStore(storeInstance);
};
If you are using a DI system, then you would NOT use createStore()
to create the store + hook. Nor would you need to manually construct the store instance (like shown above).
Instead, the DI would inject a store instance and you would use Zustand's useStore()
call:
eg.
export function useMyStore() {
const store = inject<StoreApi<MyViewModel>>(MyStoreToken);
const vm = useStore(store);
return vm;
}
Here ^, the DI system instantiates, caches, and provides read-only access to store instance; constructed with a call to buildMyStore()
.
Here is a real-world example for "document categories" feature with Apollo GraphQL query services !
Document Categories Hook + Store
If we redact some of the implementation details, we can see how easy it is to build and use a store with GraphQL.
useStore(<instance>)