v1.0.10 · MIT · ~0.7 kB gzipped

Zustand CRUD in one line

Zenty is a small TypeScript library for Zustand. You define the type, Zenty gives you the store: add, update, delete, find, bulk operations, plus loading and error state. Works with React 18, React 19 and Next.js.

products.ts
1import { createEntitiesStore } from 'zenty' 2 3type Product = { id: string; name: string; price?: number } 4 5export const useProducts = createEntitiesStore<Product>() 6 7// anywhere in your app 8useProducts.getState().add({ id: 'p1', name: 'Desk lamp', price: 39 })
  • 1 line per store
  • Fully typed
  • Loading & error included
Read the docs
Products Example Store
This is a real Zenty store running in your browser. Add a few products, edit one, delete one. Then try adding a product with an ID that already exists and watch the error field in the live state panel. That duplicate check is built in. Anything beyond that is your call.
Not LoadedReady
Live Zustand Store State
Last call: replaceAll([4 sample products])
{
  "entities": [],
  "loaded": false,
  "loading": false,
  "error": null
}
Current Products (0)
More actions

No products yet. Add some products to see them here!

Get Started with Zenty
Install and start using Zenty in your project

npm

1npm install zenty zustand

yarn

1yarn add zenty zustand

pnpm

1pnpm add zenty zustand

Same store, written twice

On the left is a hand-written Zustand store for a product list. It does exactly what Zenty does. Not more, not less. On the right is the Zenty version.

Without Zenty · 91 lines
import { create } from 'zustand'

type Product = {
  id: string
  name: string
  price?: number
  category?: string
}

type ProductState = {
  entities: Product[]
  loaded: boolean
  loading: boolean
  error: string | null
}

type ProductActions = {
  add: (item: Product) => void
  addMany: (items: Product[]) => void
  update: (id: string, patch: Partial<Product>) => void
  updateMany: (patches: (Pick<Product, 'id'> & Partial<Product>)[]) => void
  delete: (id: string) => void
  deleteMany: (ids: string[]) => void
  find: (id: string) => Product | undefined
  has: (id: string) => boolean
  clear: () => void
  replaceAll: (items: Product[]) => void
  setLoading: (loading: boolean) => void
  setError: (error: string | null) => void
}

export const useProductStore = create<ProductState & ProductActions>()((set, get) => ({
  entities: [],
  loaded: false,
  loading: false,
  error: null,

  add: (item) =>
    set((state) => {
      if (state.entities.some((e) => e.id === item.id)) {
        return { loaded: true, error: `Item with id=${item.id} already exists.` }
      }
      return { entities: [...state.entities, item], loaded: true, error: null }
    }),

  addMany: (items) =>
    set((state) => ({
      entities: [...state.entities, ...items],
      loaded: true,
      error: null,
    })),

  update: (id, patch) =>
    set((state) => ({
      entities: state.entities.map((e) => (e.id === id ? { ...e, ...patch } : e)),
      error: null,
    })),

  updateMany: (patches) =>
    set((state) => ({
      entities: state.entities.map((e) => {
        const p = patches.find((x) => x.id === e.id)
        return p ? { ...e, ...p } : e
      }),
      error: null,
    })),

  delete: (id) =>
    set((state) => ({
      entities: state.entities.filter((e) => e.id !== id),
      error: null,
    })),

  deleteMany: (ids) =>
    set((state) => ({
      entities: state.entities.filter((e) => !ids.includes(e.id)),
      error: null,
    })),

  find: (id) => get().entities.find((e) => e.id === id),

  has: (id) => get().entities.some((e) => e.id === id),

  clear: () => set({ entities: [], loaded: false, error: null }),

  replaceAll: (items) => set({ entities: items, loaded: true, error: null }),

  setLoading: (loading) => set({ loading }),

  setError: (error) => set({ error }),
}))
With Zenty · 1 line
import { createEntitiesStore } from 'zenty'

type Product = {
  id: string
  name: string
  price?: number
  category?: string
}

export const useProductStore = createEntitiesStore<Product>()

What you get

For lists (createEntitiesStore)

add, addMany, update, updateMany, delete, deleteMany, find, has, replaceAll and clear. Plus entities, loaded, loading and error.

For single objects (createEntityStore)

set, update and clear. Turn on deepMerge and nested updates keep the fields you didn't touch.

Your own ID field

Not every API calls it id. Pass idKey: 'userId' and everything is typed against that key.

Override any action

Want new items sorted by name? Pass your own add. The rest stays default.

What Zenty doesn't do

It doesn't fetch, cache or refetch data. It has no selection state. And it doesn't plug into Zustand middleware like persist or devtools yet. If you need those, a hand-written Zustand store or TanStack Query is the better fit.

Zenty FAQ

Short answers. The docs have the details.

Frequently asked questions

What is Zenty?

Zenty is an open-source TypeScript library that adds CRUD to Zustand. createEntitiesStore gives you a typed store for a list, createEntityStore one for a single object. Both come with loading, loaded and error. It's MIT-licensed and about 0.7 kB gzipped.

Is Zenty a replacement for Zustand?

No. Zenty calls Zustand's create() under the hood and hands you a normal Zustand hook. You still install Zustand, and your existing stores keep working next to the Zenty ones. So you can move one store at a time, or just use Zenty for new ones.

How do I install Zenty?

npm install zenty zustand

Zenty works with Zustand 4.4+ and 5, and React 18 or 19.

How do I manage a list?

Start with:

const useProducts = createEntitiesStore<Product>()

If your type has an id field, that's it. If the ID is called something else, pass idKey, e.g. createEntitiesStore<User, 'userId'>({ idKey: 'userId' }). One thing worth knowing: add refuses duplicates and sets error. update and delete on an ID that doesn't exist do nothing and don't report it.

How do I manage a single object, like settings or the current user?

Use createEntityStore with deepMerge on:

const useSettings = createEntityStore<Settings>({ deepMerge: true })

set replaces the object, update merges into it, clear resets it to null. With deepMerge: true, nested objects are merged instead of overwritten. In TypeScript strict mode a nested partial still needs a cast, because update is typed as Partial<T>. This is a known limitation.

How do I load data from an API?

Zenty doesn't fetch for you. You call your API and put the result into the store:

const { setLoading, setError, replaceAll } = useProducts.getState()
setLoading(true)
try {
  const res = await fetch('/api/products')
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  replaceAll(await res.json())
} catch {
  setError('Could not load products')
} finally {
  setLoading(false)
}

replaceAll and setError don't reset loading, so the finally block is still yours to write.

Does Zenty work with Next.js?

Yes, in client components ('use client'). A Zenty store is a regular module-level Zustand store, so the usual Zustand advice for Next.js applies. Don't keep per-user data in it during server rendering.

Zenty or TanStack Query?

Different jobs. TanStack Query caches server data and handles refetching, stale data and invalidation. Zenty is a plain client store you control yourself. It fits when you want the data in Zustand anyway: local edits before saving, a list fed by a WebSocket or a Firestore listener, or an admin table you update by hand. Plenty of apps use both.

Zenty or Redux Toolkit?

If you're already on Zustand, adding Redux for CRUD is a lot of setup for little gain. Redux Toolkit is a full framework with slices, middleware, devtools and RTK Query. Zenty does one thing: the CRUD part of a Zustand store. If you need devtools or time-travel debugging today, RTK is ahead.

Zenty compared

For CRUD state in a React app.

Scroll horizontally to see all columns.

CriteriaZentyZustand, hand-writtenRedux ToolkitContext + useReducer
Code for a list store1 line~90 lines (see above)slice + store setupreducer + provider
Typed actionsinferred from your typeyou write the typesyesyou write the types
Loading / error stateincludedyou write itvia thunks or RTK Queryyou write it
Data fetching & cachingnonoyes (RTK Query)no
Devtools / middlewarenot yetyesyesno
Good fit forCRUD lists and objects in Zustand appscustom store logiclarge apps, complex flowssmall, local state