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

@jasperoosthoek/zustand-crud-registry

v0.3.3

Published

Flexible library to interact with a CRUD backend using Zustand and Axios

Readme

Zustand CRUD Registry

npm Tests Coverage License: MIT TypeScript

Effortless CRUD operations for React applications with automatic state management, loading states, and type safety.

Transform your React frontend into a powerful data management interface with just a few lines of code. This library combines Zustand state management with Axios HTTP client to provide reactive, type-safe CRUD operations that automatically sync your UI with your backend.

Key Features

  • Automatic State Sync - Backend changes instantly reflect in your React components
  • Type-Safe Operations - Full TypeScript support with intelligent autocompletion
  • Built-in Loading States - No more manual loading/error state management
  • Zero Boilerplate - Set up complete CRUD operations in minutes
  • Flexible Configuration - Custom actions, routes, and data transformation
  • Ordered Storage - Map-based storage preserves backend insertion order
  • React Hooks - Clean, composable API for your components

Quick Start

Installation

npm install @jasperoosthoek/zustand-crud-registry zustand axios

Basic Setup

// stores/registry.ts
import { createStoreRegistry } from "@jasperoosthoek/zustand-crud-registry";
import axios from "axios";

// Define your data types
export type User = {
  id: number;
  name: string;
  email: string;
};

export type Post = {
  id: number;
  title: string;
  content: string;
  userId: number;
};

// Create the store registry (only once in your app)
export const getOrCreateStore = createStoreRegistry<{
  users: User;
  posts: Post;
}>();

// Set up your HTTP client
const api = axios.create({ baseURL: '/api' });

// Create your stores
export const usersStore = getOrCreateStore('users', {
  axios: api,
  route: '/users',
  actions: {
    getList: true,
    create: true,
    update: true,
    delete: true,
  },
  includeList: true,       // Include list in useCrud return
});

export const postsStore = getOrCreateStore('posts', {
  axios: api,
  route: '/posts',
  actions: {
    getList: true,
    create: true,
    update: true,
    delete: true,
  },
  includeList: true,
});

Using in Components

// components/UsersList.tsx
import React, { useEffect } from 'react';
import { useCrud } from "@jasperoosthoek/zustand-crud-registry";
import { usersStore } from '../stores/registry';

export const UsersList = () => {
  const users = useCrud(usersStore);

  useEffect(() => {
    if (!users.list) {
      users.getList();
    }
  }, []);

  const handleCreateUser = () => {
    users.create({
      name: 'New User',
      email: '[email protected]'
    });
  };

  const handleDeleteUser = (user: User) => {
    users.delete(user);
  };

  if (users.getList.isLoading) {
    return <div>Loading users...</div>;
  }

  if (users.getList.error) {
    return <div>Error: {users.getList.error.message}</div>;
  }

  return (
    <div>
      <button onClick={handleCreateUser} disabled={users.create.isLoading}>
        {users.create.isLoading ? 'Creating...' : 'Add User'}
      </button>
      
      <div>
        {users.list?.map(user => (
          <div key={user.id} className="user-item">
            <span>{user.name} - {user.email}</span>
            <button 
              onClick={() => handleDeleteUser(user)}
              disabled={users.delete.isLoading && users.delete.id === user.id}
            >
              {users.delete.isLoading && users.delete.id === user.id ? 'Deleting...' : 'Delete'}
            </button>
          </div>
        ))}
      </div>
    </div>
  );
};

Complete Examples

Advanced Store Configuration

// stores/advanced.ts
import { createStoreRegistry } from "@jasperoosthoek/zustand-crud-registry";
import axios from "axios";

export type Task = {
  id: number;
  title: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
  order: number;
};

export const getOrCreateStore = createStoreRegistry<{
  tasks: Task;
}>();

const api = axios.create({ 
  baseURL: '/api',
  headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
});

export const tasksStore = getOrCreateStore('tasks', {
  axios: api,
  route: '/tasks',
  actions: {
    getList: {
      method: 'post', // Custom HTTP method
      route: '/tasks/search', // Custom route
    },
    create: true,
    update: true,
    delete: true,
  },
  // Custom actions for additional endpoints
  customActions: {
    reorder: {
      route: ({ id }: Task) => `/tasks/${id}/reorder`,
      method: 'patch',
    },
    toggleComplete: {
      route: ({ id }: Task) => `/tasks/${id}/toggle`,
      method: 'post',
    },
  },
  includeList: true,
  // Component state (separate from server data)
  state: {
    selectedTaskId: null as number | null,
    filterBy: 'all' as 'all' | 'completed' | 'pending',
  },
  // Error handling
  onError: (error) => {
    console.error('Task operation failed:', error);
    // Could integrate with toast notifications, etc.
  },
});

Component with Custom Actions

// components/TaskManager.tsx
import React, { useEffect } from 'react';
import { useCrud } from "@jasperoosthoek/zustand-crud-registry";
import { tasksStore } from '../stores/advanced';

export const TaskManager = () => {
  const tasks = useCrud(tasksStore);

  useEffect(() => {
    tasks.getList();
  }, []);

  const handleToggleComplete = (task: Task) => {
    // Custom action usage
    tasks.toggleComplete(task);
  };

  const handleFilterChange = (filter: 'all' | 'completed' | 'pending') => {
    // Update component state
    tasks.patchState({ filterBy: filter });
  };

  const filteredTasks = tasks.list?.filter(task => {
    if (tasks.state.filterBy === 'completed') return task.completed;
    if (tasks.state.filterBy === 'pending') return !task.completed;
    return true;
  });

  return (
    <div>
      <div>
        <button onClick={() => handleFilterChange('all')}>All</button>
        <button onClick={() => handleFilterChange('completed')}>Completed</button>
        <button onClick={() => handleFilterChange('pending')}>Pending</button>
      </div>

      <div>
        {filteredTasks?.map(task => (
          <div key={task.id} className="task-item">
            <span>{task.title}</span>
            <button 
              onClick={() => handleToggleComplete(task)}
              disabled={tasks.toggleComplete.isLoading && tasks.toggleComplete.id === task.id}
            >
              {task.completed ? 'Mark Pending' : 'Mark Complete'}
            </button>
          </div>
        ))}
      </div>
    </div>
  );
};

Custom Response Handling

// hooks/useTasksWithReorder.ts
import { useCrud } from "@jasperoosthoek/zustand-crud-registry";
import { tasksStore } from '../stores/advanced';

export const useTasksWithReorder = () => {
  const tasks = useCrud(tasksStore);

  // Handle reorder response — update only the order fields without refetching
  tasks.reorder.onResponse = (reorderedTasks: Partial<Task>[]) => {
    tasksStore.patchList(reorderedTasks);
  };

  return tasks;
};

API Reference

createStoreRegistry<Models>()

Creates a store registry function. Call this only once in your application.

Parameters:

  • Models: TypeScript type defining your entity models

Returns: getOrCreateStore function

getOrCreateStore(key, config)

Creates or retrieves a store for a specific entity type.

Parameters:

  • key: String key matching your Models type
  • config: Store configuration object

Config Options:

{
  axios: AxiosInstance;           // HTTP client instance
  route: string | RouteFunction;  // Base API route
  actions?: {                     // Enable/configure CRUD operations
    getList?: boolean | ActionConfig;
    get?: boolean | ActionConfig;
    create?: boolean | ActionConfig;
    update?: boolean | ActionConfig;
    delete?: boolean | ActionConfig;
  };
  customActions?: {               // Custom API endpoints
    [name: string]: CustomActionConfig;
  };
  state?: object;                 // Component state
  onError?: (error: any) => void; // Error handler
  detailKey?: string;              // Field used in detail routes (default: 'id')
  id?: string;                    // Field used to key internal Map (default: detailKey)
  includeList?: boolean;          // Include list array in useCrud return
  includeRecord?: boolean;        // Include record object in useCrud return
  pagination?: true | {                  // Enable pagination
    limit?: number;
    offset?: number;
    prepare?: (responseData: any) => Partial<Pagination>;  // map response → { count, offset, limit }
    prepareParams?: (pagination: Pagination) => Record<string, any>; // map pagination → query params
    extractList?: (responseData: any) => any[]; // pick list from response (default: response.data)
  };
  select?: 'single' | 'multiple';      // Enable selection
}

useCrud(store)

Main hook for interacting with your store.

Returns:

{
  // Data views (opt-in via config)
  list?: T[] | null;                   // Array of entities (if includeList: true)
  record?: {[id: string]: T} | null;   // Keyed object of entities (if includeRecord: true)

  // CRUD operations (if enabled)
  getList: AsyncFunction;
  get: AsyncFunction;
  create: AsyncFunction;
  update: AsyncFunction;
  delete: AsyncFunction;

  // Custom actions
  [customAction]: AsyncFunction;

  // State management (if configured)
  state: StateObject;
  patchState: (partial: Partial<StateObject>) => void;

  // Pagination (if configured)
  pagination: { count: number; offset: number; limit: number };
  setPagination: (partial: Partial<Pagination>) => void;

  // Selection (if configured)
  selected: T | null;                  // if select: 'single'
  selected: T[];                       // if select: 'multiple'
  select: (instanceOrId: T | string | number | null) => void;
  toggle: (instanceOrId: T | string | number) => void;
  clear: () => void;

  // Loading states for each operation
  // Each operation has: isLoading, error, response, id, sequence
}

Granular Hooks

For more control, use the standalone hooks instead of useCrud:

  • useList(store) - Returns T[] | null (ordered array)
  • useRecord(store) - Returns { [key: string]: T } | null (keyed object)
  • useGet(store, id?) - Returns [instance, get] tuple; auto-fetches on mount when id is provided
  • useGetList(store) - Returns [list, getList] tuple; auto-fetches on mount
  • useSelect(store) - Returns selection state with selectedId/selectedIds
  • usePagination(store) - Returns [pagination, setPagination] tuple
  • useCrudState(store) - Returns { state, patchState } for custom component state

Store State Functions

The store exposes mutation functions directly on the store object (also available via store.getState()):

List Operations:

  • store.setList(items: T[] | null) - Replaces all data (or clears with null)
  • store.patchList(items: Partial<T>[]) - Updates existing items only (ignores new items)
  • store.updateList(items: T[]) - Upserts items (updates existing, inserts new)

Single Item Operations:

  • store.setInstance(item: T) - Adds or replaces a single item
  • store.updateInstance(item: T) - Merges updates into an existing item
  • store.deleteInstance(item: T) - Removes an item from the store

State Management:

  • store.patchState(partial: Partial<State>) - Updates custom component state (when state is configured)
  • store.setPagination(partial: Partial<Pagination>) - Updates pagination (when pagination is configured)
  • store.setSelectedIds(ids: string[]) - Updates selection (when select is configured)
  • store.getState().setLoadingState(key: string, value: Partial<LoadingState>) - Manually update loading state

Advanced Usage

Route Functions

// Dynamic routes based on data
const store = getOrCreateStore('posts', {
  axios: api,
  route: '/posts',
  actions: {
    getList: {
      route: ({ userId }: { userId: number }) => `/users/${userId}/posts`
    },
    update: {
      method: 'put',            // Use custom http method instead of default patch
      route: (post: Post) => `/posts/${post.id}/update`
    }
  }
});

Data Transformation

// Transform data before sending to API
const store = getOrCreateStore('users', {
  axios: api,
  route: '/users',
  actions: {
    create: {
      prepare: (userData) => ({
        ...userData,
        createdAt: new Date().toISOString()
      })
    }
  }
});

Error Handling

const store = getOrCreateStore('users', {
  axios: api,
  route: '/users',
  onError: (error) => {
    if (error.response?.status === 401) {
      // Handle authentication error
      redirectToLogin();
    } else {
      // Show error notification
      showErrorToast(error.message);
    }
  }
});

🛠️ Best Practices

1. Store Organization

// stores/index.ts - Export all stores from one place
export * from './users';
export * from './posts';
export * from './registry';

2. Default Configuration

// stores/config.ts - Reuse common configuration
export const defaultConfig = {
  axios: apiClient,
  actions: {
    getList: true,
    create: true,
    update: true,
    delete: true,
  },
  onError: handleApiError,
};

// stores/users.ts
export const usersStore = getOrCreateStore('users', {
  ...defaultConfig,
  route: '/users',
});

3. Custom Hooks

// hooks/useUsers.ts - Wrap logic in custom hooks
import { useList } from "@jasperoosthoek/zustand-crud-registry";

export const useUsers = () => {
  const users = useCrud(usersStore);
  const list = useList(usersStore);

  useEffect(() => {
    if (!list) {
      users.getList();
    }
  }, []);

  return { ...users, list };
};

4. Type Safety

// types/api.ts - Define your API types
export interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}

export interface CreateUserRequest {
  name: string;
  email: string;
}

Troubleshooting

Common Issues

Q: TypeScript errors about store types

// ❌ Don't do this - creates multiple registries
const store1 = createStoreRegistry<{...}>()('users', config);
const store2 = createStoreRegistry<{...}>()('posts', config);

// ✅ Do this - single registry
const getOrCreateStore = createStoreRegistry<{...}>();
const store1 = getOrCreateStore('users', config);
const store2 = getOrCreateStore('posts', config);

Q: Actions not working

// ❌ Make sure actions are enabled
const store = getOrCreateStore('users', {
  axios: api,
  route: '/users',
  // actions: { ... } // Missing actions config
});

// ✅ Enable the actions you need
const store = getOrCreateStore('users', {
  axios: api,
  route: '/users',
  actions: {
    getList: true,
    create: true,
    // ... other actions
  }
});

Q: Loading states not updating

// ❌ Don't destructure loading states
const { getList, getList: { isLoading } } = useCrud(store);

// ✅ Access loading states directly
const users = useCrud(store);
const isLoading = users.getList.isLoading;

Examples & Demos

Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development Setup

# Clone the repository
git clone https://github.com/jasperoosthoek/zustand-crud-registry.git

# Install dependencies
npm install

# Build the project
npm run build

# Run tests
npm test

License

MIT © jasperoosthoek

Acknowledgments

  • Built with Zustand for state management
  • HTTP client powered by Axios
  • Inspired by the need for simpler CRUD operations in React applications

Made with ❤️ by @jasperoosthoek