npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

trpc-zustand

v1.1.1

Published

Zustand bindings for tRPC. Manage tRPC queries, mutations and subscriptions as Zustand stores.

Readme

trpc-zustand

Zustand bindings for tRPC clients. Manage tRPC queries, mutations and subscriptions as Zustand stores.

trpc-zustand bridges the gap between tRPC and Zustand, allowing you to use Zustand-specific patterns to manage tRPC operations and their state, including middlewares, store slicing, selectors, React integration, and more. Fully typed and compatible with tRPC v11 and Zustand v5.

Installation

Install trpc-zustand and its peer dependencies @trpc/client, @trpc/server, and zustand:

npm install trpc-zustand @trpc/client @trpc/server zustand
yarn add trpc-zustand @trpc/client @trpc/server zustand
bun add trpc-zustand @trpc/client @trpc/server zustand

Setup

Create a tRPC client with createTRPCZustand:

import {
  splitLink,
  unstable_httpBatchStreamLink,
  unstable_httpSubscriptionLink
} from "@trpc/client";
import { createTRPCZustand } from "trpc-zustand";
import { create } from "zustand";
import type { AppRouter } from "./server";

const trpc = createTRPCZustand<AppRouter>({
  links: [
    splitLink({
      condition: op => op.type === "subscription",
      // In case you want subscriptions:
      true: unstable_httpSubscriptionLink({ url: "http://localhost:2022" }),
      false: unstable_httpBatchStreamLink({ url: "http://localhost:2022" })
    })
  ]
});

Then create Zustand stores using the client, one store per tRPC operation. The client returns Zustand state creators (the function that takes set and get) so it works just fine with both React- and vanilla Zustand:

import { create } from "zustand"; // React

export const useTodosStore = create(trpc.todos.getTodos.queryStore());
export const useAddTodoStore = create(trpc.todos.addTodo.mutationStore());
import { createStore } from "zustand"; // Vanilla

export const todosStore = createStore(trpc.todos.getTodos.queryStore());
export const addTodoStore = createStore(trpc.todos.addTodo.mutationStore());

You can pass initialization options to the store creator:

export const useTodosStore = create(
  trpc.todos.getTodos.queryStore({ keepPreviousData: false })
);

export const useAddTodoStore = create(
  trpc.todos.addTodo.mutationStore({ refetchStores: () => [useTodosStore] })
);

Guides

Vanilla demo

Here is an overview of the API that the tRPC stores expose. For an exhaustive list of methods and properties, see the API Reference.

import { createTRPCZustand } from "trpc-zustand";
import { createStore } from "zustand";
import type { AppRouter } from "./server";

const trpc = createTRPCZustand<AppRouter>({
  /* ... */
});

const todosStore = createStore(trpc.todos.getTodos.queryStore());
const addTodoStore = createStore(trpc.todos.addTodo.mutationStore());

// Fetch data
todosStore.getState().query({ listId: "1" });

// Refetch data (last input)
todosStore.getState().refetch();

// Disable/enable/reset a store
todosStore.getState().disable();
todosStore.getState().enable();
todosStore.getState().reset();

// Execute a mutation
addTodoStore.getState().mutate({ listId: "1", title: "Buy milk" });

// Lookup a store's state
const { data, loading, error } = todosStore.getState();
const isAddingTodo = addTodoStore.getState().loading;

// Listen to state changes
const unsubscribe = addTodoStore.subscribe((state, prevState) => {
  if (state.loading && !prevState.loading) {
    console.log("Adding todo...");
  }
});

React demo

Everything that works with vanilla stores works on React stores too. But in addition to that, you can use the store as a hook with or without custom selectors. The component will re-render when the selected store state changes. Note: if you only need methods, there is no need to use it as a hook as the methods never change.

import { createTRPCZustand } from "trpc-zustand";
import { create } from "zustand";
import type { AppRouter } from "./server";

const trpc = createTRPCZustand<AppRouter>({
  /* ... */
});

const useTodosStore = create(trpc.todos.getTodos.queryStore());
const useAddTodoStore = create(trpc.todos.addTodo.mutationStore());

function TodoList({ listId }: { listId: string }) {
  // Use the entire store
  const todos = useTodosStore();

  useEffect(() => {
    // Fetch data
    todos.query({ listId });
  }, [listId]);

  const addTodo = async () => {
    // Execute a mutation
    await useAddTodoStore.getState().mutate({ listId, title: "Buy milk" });
    // Refetch data
    await todos.refetch();
  };

  // Display query states and data
  return (
    <div>
      {todos.loading ? (
        <p>Loading...</p>
      ) : todos.error ? (
        <p>Error: {todos.error.message}</p>
      ) : (
        todos.data && (
          <ul>
            {todos.data.map(todo => (
              <li key={todo.id}>{todo.title}</li>
            ))}
          </ul>
        )
      )}
      <button onClick={addTodo}>Add todo</button>
      <Loader />
    </div>
  );
}

function Loader() {
  // Use custom selectors
  const addTodoLoading = useAddTodoStore(state => state.loading);

  return <p>addTodo is {addTodoLoading ? "loading" : "not loading"}</p>;
}

Zustand middlewares

Zustand middlewares will work like with any other Zustand store. The following example persists the data of the latest getTodos query in localStorage. When you reload the page, the data will be restored without having to refetch it.

import { create } from "zustand";
import { persist } from "zustand/middleware";

const useTodosStore = create(
  persist(trpc.todos.getTodos.queryStore(), {
    name: "todos",
    partialize: ({ data }) => ({ data })
  })
);

Refetch after mutation

You can refetch a store after a mutation by passing a refetchStores option to the mutation store:

const useTodosStore = create(trpc.todos.getTodos.queryStore());

const useAddTodoStore = create(
  trpc.todos.addTodo.mutationStore({ refetchStores: () => [useTodosStore] })
);

// Will refetch the todos store after the mutation is successful
useAddTodoStore.getState().mutate({ listId: "1", title: "Buy milk" });

refetchStores is a function that is called after the mutation is successful. Thus you can have arbitrary control over which stores are refetched and when:

const useAddTodoStore = create(
  trpc.todos.addTodo.mutationStore({
    refetchStores: () => {
      if (
        useAddTodoStore.getState().input?.listId ===
        useTodosStore.getState().input?.listId
      ) {
        return [useTodosStore];
      }
    }
  })
);

If you want to refetch after specific mutation calls only and not as a general rule, just use promise patterns:

const addTodo = async () => {
  await useAddTodoStore.getState().mutate({ listId: "1", title: "Buy milk" });
  await useTodosStore.getState().refetch();
};

Store slicing

You can compose a tRPC store with other Zustand stores using the slice pattern, as long as the properties don't conflict.

import { create, StateCreator } from "zustand";
import { createTRPCZustand, InferStore } from "trpc-zustand";
import type { AppRouter } from "./server";

const trpc = createTRPCZustand<AppRouter>({
  /* ... */
});

// Use the InferStore utility to get the store's exact state type
type DeleteTodoSlice = InferStore<typeof deleteTodoSlice>;

type AdditionalSlice = {
  foo: string;
  resetFoo: () => void;
};

const deleteTodoSlice = trpc.todos.deleteTodo.mutationStore();

const additionalSlice: StateCreator<
  DeleteTodoSlice & AdditionalSlice,
  [],
  [],
  AdditionalSlice
> = (set, get) => ({
  foo: "bar",
  resetFoo: () => set({ foo: get().loading ? "bar" : "baz" })
});

export const useDeleteTodoStore = create<DeleteTodoSlice & AdditionalSlice>(
  (...a) => ({
    ...deleteTodoSlice(...a),
    ...additionalSlice(...a)
  })
);

Subscriptions

You can use the subscribe method to start a subscription. It returns a cancellation function.

const streamTodosStore = createStore(
  trpc.todos.streamTodos.subscriptionStore()
);

// Start a tRPC subscription
const stop = streamTodosStore.getState().subscribe(
  { listId },
  {
    onData: data => {
      console.log("New todo:", data);
    },
    onError: error => {
      console.error("Error:", error);
    }
  }
);

// Stop the subscription
stop();

React-style:

const useStreamTodosStore = create(trpc.todos.streamTodos.subscriptionStore());

function TodoStream({ listId }: { listId: string }) {
  const { status, data, error, subscribe } = useStreamTodosStore();

  useEffect(() => {
    // Start a tRPC subscription
    // Note: returns the cancellation function as cleanup
    return subscribe({ listId });
  }, [listId]);

  return (
    <div>
      {error && <p>Error: {error.message}</p>}
      <p>
        ({status}) Latest todo: {data?.title}
      </p>
    </div>
  );
}

API Reference

Queries

Initialization options

| Property | Type | Description | | ------------------ | --------- | ---------------------------------------------------------------------------------------- | | enabled | boolean | Initially enable/disable the store. Optional. Defaults to true. | | keepPreviousData | boolean | Keep the previous data while a new query is being fetched. Optional. Defaults to true. |

Store

| Property | Type | Description | | ------------------ | ------------------------------ | ------------------------------------------------------------------------------------------------- | | enabled | boolean | Tracks the store's enabled state. | | keepPreviousData | boolean | Tracks if the previous data should be kept while a new query is being fetched. | | input | TInput \| undefined | The input of the last query. | | loading | boolean | Tracks the query's loading state. | | data | TData \| undefined | The data returned by the last query. | | error | TRPCClientError \| undefined | The error thrown by the last query. | | enable | () => void | Enables the store. | | disable | () => void | Disables the store. | | toggle | () => void | Toggles the store's enabled state. | | reset | () => void | Resets the store. | | query | (input, opts?) => Promise | Fires a query. If there is an ongoing query, it is stopped and the new one is started. | | refetch | (opts?) => Promise | Refetches the last query. If there is an ongoing query, it is stopped and the new one is started. |

Mutations

Initialization options

| Property | Type | Description | | --------------- | ------------------ | ------------------------------------------------------------------------------------ | | enabled | boolean | Initially enable/disable the store. Optional. Defaults to true. | | refetchStores | () => StoreApi[] | Calls the refetch method on the stores after the mutation is successful. Optional. |

Store

| Property | Type | Description | | --------- | ------------------------------ | -------------------------------------------------------------------------------------------- | | enabled | boolean | Tracks the store's enabled state. | | input | TInput \| undefined | The input of the last mutation. | | loading | boolean | Tracks the mutation's loading state. | | data | TData \| undefined | The data returned by the last mutation. | | error | TRPCClientError \| undefined | The error thrown by the last mutation. | | enable | () => void | Enables the store. | | disable | () => void | Disables the store. | | toggle | () => void | Toggles the store's enabled state. | | reset | () => void | Resets the store. | | mutate | (input, opts?) => Promise | Fires a mutation. If there is an ongoing mutation, it is stopped and the new one is started. |

Subscriptions

Initialization options

| Property | Type | Description | | --------- | --------- | ----------------------------------------------------------------- | | enabled | boolean | Initially enable/disable the store. Optional. Defaults to true. |

Store

| Property | Type | Description | | ----------- | ------------------------------------------------ | --------------------------------------------------------------------- | | enabled | boolean | Tracks the store's enabled state. | | input | TInput \| undefined | The input of the last subscription. | | status | "idle" \| "connecting" \| "pending" \| "error" | Tracks the subscription's status. | | data | TData \| undefined | The last data returned by the subscription. | | error | TRPCClientError \| undefined | The last error thrown by the subscription. | | enable | () => void | Enables the store. | | disable | () => void | Disables the store. | | toggle | () => void | Toggles the store's enabled state. | | reset | () => void | Resets the store. | | subscribe | (input, opts?) => () => void | Subscribes to the last subscription. Returns a cancellation function. |

Contributing

Contributions are welcome! Please open an issue or submit a pull request for any features, bug fixes, or improvements.

License

MIT