Skip to content

Instantly share code, notes, and snippets.

@ThomasBurleson
Last active August 12, 2025 16:04
Show Gist options
  • Save ThomasBurleson/572e4a8c5a84f0d5e48fb55228cc87e7 to your computer and use it in GitHub Desktop.
Save ThomasBurleson/572e4a8c5a84f0d5e48fb55228cc87e7 to your computer and use it in GitHub Desktop.

Zustand Stores

In its simplest form, zustand is easily scaffolded:

Raw ES6 Stores

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.

Using with React

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.

Remote Services

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);
};

Using with React + DI features

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().

@ThomasBurleson
Copy link
Author

ThomasBurleson commented Aug 10, 2025

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.

  • Build Dataservice instance
  • Build Store instance
  • Publish store with useStore(<instance>)
// **********************************************************************************************
// Category Store Hook
// **********************************************************************************************

export const useCategoriesStore = (orgId: string | undefined, documents: Document[]) => {
  const store = makeStore();

  useEffect(() => {
    const {api} = store.getState();
    api.loadAll(orgId, documents || []);
  }, []);

  return useStore(store);
};

// **********************************************************************************************
// Build Store Singleton
// **********************************************************************************************

const buildCategoriesStore = (service: DocumentsDataService, t: TranslateFn) => {
  const configureStore = (set: (state: any) => any): CategoriesViewModel => {
    const state = { };
    const api = { };

    return { ...state, api };
  };

  return createStore<CategoriesViewModel>()(
    computed( buildComputed, configureStore )
  );
};

// Normally done with DI
const makeStore = (): StoreApi<CategoriesViewModel> => {
  if (!singleton) {
    const t = inject<TranslateFn>(i18nTranslateFn);
    const service = buildDataService();

    singleton = buildCategoriesStore(service, t);
  }
  return singleton;
};

// **********************************************************************************************
// Build Remote DataServices (CRUD) API
// **********************************************************************************************

const buildDataService = (): DocumentsDataService => {
  return {
    getAllCategories: async (orgId: string) => { },
  }
};

@ThomasBurleson
Copy link
Author

ThomasBurleson commented Aug 11, 2025

Full Implementation

Notice that much of this code is definitions of types and instance/factories.

categories.store.ts
import { useEffect } from "react";

import { ApolloClient } from "@apollo/client";
import { createStore, StoreApi, useStore } from "zustand";

import { AuthenticatedClient, buildApolloQuery, computed, i18nTranslateFn, inject } from "@rr/core";
import { TranslateFn } from "@rr/hooks";
import { Document, GetAllDocumentCategories } from "@rr/rsm";

import { DocumentCategory } from "rsm/stores/person/person.model";

// **********************************************************************************************
// Zustand store for managing documents categories state
// **********************************************************************************************

/**
 * Categories store hook for Documents Sidebar
 */
export const useCategoriesStore = (orgId: string | undefined, documents: Document[]) => {
  const store = makeStore();

  useEffect(() => {
    const { api, isLoading, documents: currentDocs, orgId: currentOrgId } = store.getState();
    const shouldReload = !!orgId && orgId !== currentOrgId && !currentDocs.length;

    if (!isLoading && shouldReload) {
      api.loadAll(orgId, documents || []);
    }
  }, [orgId, documents, store]);

  return useStore(store);
};

// **********************************************************************************************
// Zustand store for managing documents categories state
// **********************************************************************************************

export interface CategoriesStoreState {
  documents: Readonly<Document[]>;
  orgId: string | null | undefined;

  categories: DocumentCategory[];
  selectedId: string | null;
  isLoading: boolean;
}
export interface CategoriesStoreAPI {
  loadAll: (orgId: string | undefined, knownDocuments: Document[]) => Promise<boolean>;
  select: (selectedId?: string | null) => void;
}
export interface CategoriesComputedState {
  selected: any;
}
export type CategoriesViewModel = CategoriesStoreState & CategoriesComputedState & { api: CategoriesStoreAPI };

// ***********************************************
// Singleton Cache
// Requird, since we are not using DI for this store
// ***********************************************

let singleton: any = null;

/**
 * This creates a singleton store instance
 * !! Not exported because it is ONLY used by useDocumentsSidebar()
 */
const makeStore = (): StoreApi<CategoriesViewModel> => {
  if (!singleton) {
    const t = inject<TranslateFn>(i18nTranslateFn);
    const service = buildDataService();

    singleton = buildCategoriesStore(service, t);
  }
  return singleton;
};

// **********************************************************************************************
// Internal Store Factory and utils
// **********************************************************************************************

/**
 * Store Factory function
 */
const buildCategoriesStore = (service: DocumentsDataService, t: TranslateFn) => {
  const configureStore = (set: (state: any) => any): CategoriesViewModel => {
    const state = {
      documents: [],
      orgId: undefined,
      categories: [],
      selectedId: null,
    };
    const api = {
      loadAll: async (orgId: string | undefined, documents: Document[] = []) => {
        set({ isLoading: true, documents, categories: [] }); // clear list immediately...

        const [rawList, error] = await service.getAllCategories(orgId || "");
        const categories = toCategories(rawList, documents, t);

        set({ orgId, categories, isLoading: false });
        return !error;
      },
      select: (selectedId?: string | null) => set({ selectedId: selectedId || null }),
    };

    return { ...state, isLoading: false, selected: null, api };
  };

  return createStore<CategoriesViewModel>()(computed(compute, configureStore));
};

/**
 * Dynamically update computed properties
 */
const compute = (state: CategoriesViewModel): Partial<CategoriesViewModel> => {
  const selected = state.categories?.find((s) => s.id === state.selectedId);

  return { selected };
};


// **********************************************************************************************
// Remote DataServices (CRUD) API
// **********************************************************************************************

export interface DocumentsDataService {
  getAllCategories: (orgId: string) => Promise<[GetAllDocumentCategories.All[], DataserviceError]>;
}

export type DocumentDTO = GetAllDocumentCategories.All;

/**
 * Factory function to build the authenticated DataService instance
 * NOTE: this uses special `buildApolloQuery()` instead of Apollo Query hooks
 */
const buildDataService = () => {
  const service: DocumentsDataService = {
    getAllCategories: async (orgId: string) => {
      const apollo = inject<AuthenticatedClient>(ApolloClient);
      const doQuery = buildApolloQuery<
        GetAllDocumentCategories.Query,
        GetAllDocumentCategories.All[],
        GetAllDocumentCategories.Variables
      >(apollo);

      const variables = { orgId } as const;
      const query = GetAllDocumentCategories.Document;
      const selector = (response: any): GetAllDocumentCategories.All[] => {
        return compact(response.data?.documentCategory?.all);
      };

      const [list, error] = await doQuery(query, selector, variables);
      return [list || [], error];
    },
  };

  return service;
};

// ***********************************************
// Utils (exported for unit tests)
// ***********************************************

/**
 * Process raw category list and compute usage counts
 */
const toCategories = (rawList: DocumentDTO[], knownDocuments: Document[] = [], t: TranslateFn): any[] => {
  const categoryCountMap = new Map<string, number>();

  knownDocuments.forEach((doc) => {
    if (doc.category) {
      const existing = categoryCountMap.get(doc.category.id);
      categoryCountMap.set(doc.category.id, (existing || 0) + 1);
    }
  });

  // Create category list with "All" at the top,
  // custom ones in the middle alphabetically sorted,
  // and "Uncategorized" at the end

  const categoryList = rawList
    .map(
      (category: any): DocumentCategory => ({
        id: category.id,
        name: category.name,
        usageCount: categoryCountMap.get(category.id) || 0,
      }),
    )
    .sort((a: DocumentCategory, b: DocumentCategory) => a.name.localeCompare(b.name));

  categoryList.push({
    id: "uncategorized",
    name: t("Uncategorized"),
    usageCount: knownDocuments.filter((doc) => !doc.category).length,
  });

  return categoryList;
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment