Zenty – Zustand CRUD Made Simple
What is Zenty? Zenty is an open-source TypeScript library built on Zustand for React and Next.js CRUD state. Replace 50+ lines of boilerplate with 1 line —createEntitiesStore for lists, createEntityStore for single objects — with loading, loaded and error states built-in.
- 1 line for full CRUD
- Type-safe by default
- Loading / error built-in
error
error
error
error
{
"entities": [],
"loaded": false,
"loading": false,
"error": null
}No products yet. Add some products to see them here!
npm
1npm install zenty zustandyarn
1yarn add zenty zustandpnpm
1pnpm add zenty zustand
type Product {
id: string
name: string
price?: number
category?: string
}
type ProductState {
entities: Product[]
loaded: boolean
loading: boolean
error: string | null
selectedId: number | null
}
type ProductActions {
add: (item: Product) => void
addMany: (items: Product[]) => void
update: (id: number, patch: Partial<Product>) => void
updateMany: (updates: Array<{ id: number; patch: Partial<Product> }>) => void
delete: (id: number) => void
deleteMany: (ids: number[]) => void
find: (id: number) => Product | undefined
has: (id: number) => boolean
clear: () => void
replaceAll: (items: Product[]) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
setSelected: (id: number | null) => void
getSelected: () => Product | undefined
}
// store.ts
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
const useProductStore = create<ProductState & ProductActions>()(
subscribeWithSelector((set, get) => ({
// State
entities: [],
loaded: false,
loading: false,
error: null,
selectedId: null,
// Actions
add: (item) => set((state) => ({
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) => {
const index = state.entities.findIndex(item => item.id === id)
if (index === -1) {
return { error: `Product with id ${id} not found` }
}
const newEntities = [...state.entities]
newEntities[index] = { ...newEntities[index], ...patch }
return { entities: newEntities, error: null }
}),
updateMany: (updates) => set((state) => {
const newEntities = [...state.entities]
let hasError = false
updates.forEach(({ id, patch }) => {
const index = newEntities.findIndex(item => item.id === id)
if (index !== -1) {
newEntities[index] = { ...newEntities[index], ...patch }
} else {
hasError = true
}
})
return {
entities: newEntities,
error: hasError ? 'Some updates failed' : null
}
}),
delete: (id) => set((state) => {
const exists = state.entities.some(item => item.id === id)
if (!exists) {
return { error: `Product with id ${id} not found` }
}
return {
entities: state.entities.filter(item => item.id !== id),
selectedId: state.selectedId === id ? null : state.selectedId,
error: null
}
}),
deleteMany: (ids) => set((state) => ({
entities: state.entities.filter(item => !ids.includes(item.id)),
selectedId: ids.includes(state.selectedId!) ? null : state.selectedId,
error: null
})),
find: (id) => get().entities.find(item => item.id === id),
has: (id) => get().entities.some(item => item.id === id),
clear: () => set({
entities: [],
loaded: false,
error: null,
selectedId: null
}),
replaceAll: (items) => set({
entities: items,
loaded: true,
error: null,
selectedId: null
}),
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
setSelected: (id) => set({ selectedId: id }),
getSelected: () => {
const state = get()
return state.selectedId ? state.find(state.selectedId) : undefined
}
}))
)
// Custom hooks for selectors
export const useProducts = () => useProductStore(state => state.entities)
export const useProductsLoaded = () => useProductStore(state => state.loaded)
export const useProductsLoading = () => useProductStore(state => state.loading)
export const useProductsError = () => useProductStore(state => state.error)
export const useSelectedProduct = () => useProductStore(state => state.getSelected())
// 150+ lines of boilerplate code just for basic CRUD!
import { createEntitiesStore } from 'zenty'
type Product {
id: string
name: string
price?: number
category?: string
}
const useProductStore = createEntitiesStore<Product>();
- • No boilerplate - Just one line to create a full-featured store
- • Built-in validation - Error handling and loading states included
- • TypeScript first - Full type safety out of the box
- • Powerful operations - CRUD, search, bulk operations, and more
- • Developer friendly - Intuitive API that just works
Zenty FAQ – Zustand CRUD, answered
Direct answers for developers and AI search. Zenty is the missing CRUD helper for Zustand: 1 type-safe line for lists and single objects.
What is Zenty?
Zenty is an open-source TypeScript-first CRUD helper built on Zustand for React and Next.js. It replaces 50+ lines of repetitive store code with createEntitiesStore for collections and createEntityStore for single objects, with loading, loaded and error states built-in.
How is Zenty different from Zustand?
Zenty is not a new state manager — it is the missing CRUD layer on top of Zustand. Zustand gives you create(); Zenty gives you add, update, delete, find, bulk ops and loading/error helpers in one line, fully type-safe.
How do I install Zenty?
Run npm install zenty zustand, then import { createEntitiesStore, createEntityStore } from 'zenty'. It works side-by-side with your existing Zustand stores, so you can migrate file-by-file.
How do I manage a list with Zenty?
Call const useProductStore = createEntitiesStore<Product>(). You instantly get entities, loading, error plus add, addMany, update, updateMany, delete, deleteMany, find, has, replaceAll and clear. Supports custom idKey like userId and custom CRUD overrides.
How do I manage a single object with Zenty?
Call const useUserStore = createEntityStore<User>({ deepMerge: true }). You get entity plus set, update and clear. With deepMerge, nested updates like update({ preferences: { theme: 'dark' } }) preserve the rest of the object.
Does Zenty work with Next.js and React 19?
Yes. Zenty is built for modern React 19 and Next.js, hydration-safe, and TypeScript-first. Use it for dashboards, admin panels, data tables, user management and any client CRUD UI-state.
How does Zenty handle loading and errors?
Every store ships with loading, loaded and error plus setLoading and setError helpers. Wrap your fetch in try/catch, call setLoading(true), then replaceAll(data) or add(data) on success, setError(message) on failure — no manual flags.
Zenty vs Redux Toolkit?
For Zustand users who need CRUD, Zenty is 1-2 lines vs 50+ lines with vanilla Zustand or a full Redux Toolkit slice with reducers and thunks. Redux Toolkit is a full state framework; Zenty is a minimal, unopinionated CRUD helper if you already chose Zustand for its simplicity and speed.
| Criteria | Zenty | Zustand vanilla | Redux Toolkit | Context + Reducer |
|---|---|---|---|---|
| Lines for full CRUD | 1–2 | 50+ | 80+ | 60+ |
| Learning curve | 2 minutes | Low, but repetitive | Medium-high | Medium, boilerplate-heavy |
| TypeScript support | First-class, inferred | Manual typing | Good, verbose | Manual, error-prone |
| Loading / error built-in | Yes | No | Via extra code | No |
| Best for | Zustand CRUD in React | Custom tiny stores | Large Redux apps | Small local state |