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

@skylabs-digital/react-proto-kit

v2.2.0

Published

React prototyping kit for rapid API development - from idea to working prototype in minutes

Readme

⚡ React Proto Kit

From idea to working prototype in minutes

npm version TypeScript React Zod Tests License: MIT

A powerful React toolkit that eliminates boilerplate and accelerates development.
Build full-stack apps with type-safe APIs, real-time state, and automatic CRUD — all from a single schema.

Quick Start · API Reference · Examples · Full Docs


⚠️ v2.0.0 breaking change. Mutation hooks (useCreate, useUpdate, usePatch, useDelete, useSingleRecord*) now return Promise<ApiResponse<T>> and never throw. Migrate from try { await mutate() } catch to const res = await mutate(); if (!res.success) { ... }. See the Migration Guide and the Structured Error Handling section below.


🚀 Quick Start

npm install @skylabs-digital/react-proto-kit zod react react-router-dom
import { createDomainApi, z } from '@skylabs-digital/react-proto-kit';

// 1️⃣ Define your schema
const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(0),
});

// 2️⃣ Create a fully functional API
const userApi = createDomainApi('users', userSchema, userSchema);

// 3️⃣ Use it in your component
function UserList() {
  const { data: users, loading } = userApi.useList();
  const { mutate: createUser } = userApi.useCreate();

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <button onClick={() => createUser({ name: 'John', email: '[email protected]', age: 30 })}>
        Add User
      </button>
      {users?.map(user => (
        <div key={user.id}>{user.name} - {user.email}</div>
      ))}
    </div>
  );
}

That's it! Full CRUD API with TypeScript inference, cache management, and automatic state sync.


✨ Features at a Glance

| Category | Features | |----------|----------| | 🔥 Zero Boilerplate | One function creates a complete CRUD API | | 🎯 Type-Safe | Full TypeScript inference from Zod schemas — ExtractEntityType, ExtractInputType | | ⚡ Real-time State | Automatic synchronization across components via GlobalStateProvider | | 🔄 Cache Invalidation | Mutations update the cache directly and trigger background refetches via the invalidation manager; imperative control via useInvalidation() | | 🌐 Backend Agnostic | FetchConnector for REST APIs, LocalStorageConnector for prototyping | | 🛡️ Structured Errors | Full ErrorResponse propagation with custom data field for rich error handling | | 📝 Form Handling | Built-in validation with useFormData and createFormHandler | | 🔗 Nested Resources | Path templates like todos/:todoId/comments with builder pattern | | 📊 Query Params | Static and dynamic query parameter management | | 🔍 URL State | useUrlTabs, useUrlModal, useUrlDrawer, useUrlStepper, useUrlAccordion, useUrlParam | | 🎭 Data Orchestrator | Aggregate multiple APIs with isLoading / isFetching / stale-while-revalidate | | 🍞 Snackbar | Built-in toast notifications with queue management | | 📡 Single Record APIs | createSingleRecordApi for settings, profiles, stats endpoints | | 📖 Read-Only APIs | createReadOnlyApi / createSingleRecordReadOnlyApi for list or single-record GET-only endpoints | | 🐛 Debug Logging | Configurable request/response logging via configureDebugLogging | | 🌱 Seed Data | Built-in seed helpers for development and testing |


📖 Table of Contents

📦 Installation

# npm
npm install @skylabs-digital/react-proto-kit zod react react-router-dom

# yarn
yarn add @skylabs-digital/react-proto-kit zod react react-router-dom

# pnpm
pnpm add @skylabs-digital/react-proto-kit zod react react-router-dom

Peer Dependencies:

| Package | Version | |---------|---------| | react | >= 16.8.0 | | react-router-dom | >= 6.0.0 | | zod | >= 3.0.0 |


🎯 Basic Usage

1️⃣ Setup Providers

import { BrowserRouter } from 'react-router-dom';
import { ApiClientProvider, GlobalStateProvider } from '@skylabs-digital/react-proto-kit';

function App() {
  return (
    <BrowserRouter>
      <ApiClientProvider
        connectorType="fetch"
        config={{ baseUrl: 'http://localhost:3001/api' }}
      >
        <GlobalStateProvider>
          {/* Your app components */}
        </GlobalStateProvider>
      </ApiClientProvider>
    </BrowserRouter>
  );
}

💡 The baseUrl is slash-agnostic — http://localhost:3001/api and http://localhost:3001/api/ both work identically.

2️⃣ Define Your Schema

import { z } from '@skylabs-digital/react-proto-kit';

// Only define business fields — id, createdAt, updatedAt are auto-generated
const todoSchema = z.object({
  text: z.string().min(1, 'Todo text is required'),
  completed: z.boolean(),
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
});

3️⃣ Create Your API

import { createDomainApi } from '@skylabs-digital/react-proto-kit';

const todoApi = createDomainApi('todos', todoSchema, todoSchema, {
  cacheTime: 5 * 60 * 1000, // 5 minutes
});

4️⃣ Use in Components

function TodoApp() {
  const { data: todos, loading, error } = todoApi.useList();
  const { mutate: createTodo } = todoApi.useCreate();
  const { mutate: updateTodo } = todoApi.useUpdate();
  const { mutate: deleteTodo } = todoApi.useDelete();

  if (loading) return <div>Loading todos...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <button onClick={() => createTodo({ text: 'New Todo', completed: false })}>
        Add Todo
      </button>
      {todos?.map(todo => (
        <div key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={() => updateTodo(todo.id, { ...todo, completed: !todo.completed })}>
            Toggle
          </button>
          <button onClick={() => deleteTodo(todo.id)}>Delete</button>
        </div>
      ))}
    </div>
  );
}

🚀 Advanced Features

🔗 Nested Resources & Builder Pattern

// Comments belong to todos
const commentApi = createDomainApi(
  'todos/:todoId/comments',
  commentSchema,
  commentUpsertSchema,
  {
    queryParams: {
      static: { include: 'author' },
      dynamic: ['status', 'sortBy']
    }
  }
);

// Usage with builder pattern
function TodoComments({ todoId }: { todoId: string }) {
  const api = commentApi
    .withParams({ todoId })
    .withQuery({ status: 'published', sortBy: 'createdAt' });
    
  const { data: comments } = api.useList();
  const { mutate: createComment } = api.useCreate();
  
  return (
    <div>
      {comments?.map(comment => (
        <div key={comment.id}>{comment.text}</div>
      ))}
      <button onClick={() => createComment({ text: 'New comment', authorId: 'user-1' })}>
        Add Comment
      </button>
    </div>
  );
}

🔀 Separate Schemas for Entity vs Input

// Full entity schema (includes server-generated fields)
const userEntitySchema = z.object({
  name: z.string(),
  email: z.string().email(),
  avatar: z.string().url(), // Server-generated
  lastLoginAt: z.string().datetime(), // Server-managed
});

// Schema for create/update operations (excludes server-generated fields)
const userUpsertSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

const userApi = createDomainApi('users', userEntitySchema, userUpsertSchema);

🏷️ Type Extraction

import { ExtractEntityType, ExtractInputType } from '@skylabs-digital/react-proto-kit';

type User = ExtractEntityType<typeof userApi>;
// Result: { id: string; createdAt: string; updatedAt: string; name: string; email: string; avatar: string; lastLoginAt: string; }

type UserInput = ExtractInputType<typeof userApi>;
// Result: { name: string; email: string; }

🛡️ Structured Error Handling

Every mutation hook resolves to a discriminated ApiResponse<T>. ErrorResponse is itself a discriminated union keyed by kind, so you narrow once and get variant-specific fields back from TypeScript:

import { ErrorResponse } from '@skylabs-digital/react-proto-kit';

// Backend returns HTTP 409:
// { message: "Stock exceeded", code: "STOCK_EXCEEDED", items: [...], orderId: "order-123" }

const res = await createMutation.mutate(checkoutData);
if (!res.success) {
  if (res.kind === 'http' && res.code === 'STOCK_EXCEEDED') {
    // Extra non-standard body fields land in `details`
    const items = (res.details as { items: StockExceededItem[] }).items;
    showStockExceededDialog(items);
  }
  return;
}
// res.data is the created entity on success

ErrorResponse shape:

type ErrorResponse =
  | { success: false; kind: 'validation'; message: string; fields: Record<string, string>; details?: unknown }
  | { success: false; kind: 'auth';       message: string; details?: unknown }
  | { success: false; kind: 'notFound';   message: string; details?: unknown }
  | { success: false; kind: 'timeout';    message: string }
  | { success: false; kind: 'network';    message: string; details?: unknown }
  | { success: false; kind: 'http';       message: string; status: number; code?: string; details?: unknown }
  | { success: false; kind: 'unknown';    message: string; details?: unknown };

💡 Status 401/403 → auth, 404 → notFound, 422 or validation map → validation, AbortErrortimeout, other non-OK HTTP → http (with status + optional code). Extras from the backend body are preserved under details on http errors.

See the full Error Handling Guide and the v2 → v3 Migration Guide for patterns and the mechanical field mapping.

📝 Form Integration

import { useFormData } from '@skylabs-digital/react-proto-kit';

function UserForm() {
  const { mutate: createUser } = userApi.useCreate();
  const { values, errors, handleInputChange, handleSubmit, reset } = useFormData(
    userUpsertSchema,
    { name: '', email: '' }
  );

  const onSubmit = handleSubmit(async (data) => {
    const res = await createUser(data);
    if (!res.success) return; // Bail out; createUser.error exposes the ErrorResponse for rendering
    reset();
  });

  return (
    <form onSubmit={onSubmit}>
      <input
        name="name"
        value={values.name || ''}
        onChange={handleInputChange}
        placeholder="Name"
      />
      {errors.name && <span>{errors.name}</span>}
      
      <input
        name="email"
        value={values.email || ''}
        onChange={handleInputChange}
        placeholder="Email"
      />
      {errors.email && <span>{errors.email}</span>}
      
      <button type="submit">Create User</button>
    </form>
  );
}

🔍 URL State Management

import { useUrlParam } from '@skylabs-digital/react-proto-kit';

function TodoList() {
  const [filter, setFilter] = useUrlParam('filter');
  const [pageParam, setPage] = useUrlParam('page');
  const page = parseInt(pageParam || '1', 10);

  const { data: todos } = todoApi.useList({
    page,
    limit: 10,
    filters: { status: filter || 'all' },
  });

  return (
    <div>
      <button onClick={() => setFilter('active')}>Show Active</button>
      <button onClick={() => setFilter('completed')}>Show Completed</button>
      <button onClick={() => setPage(String(page + 1))}>Next Page</button>
      {/* Render todos */}
    </div>
  );
}

💡 For richer URL-driven UI primitives, see useUrlTabs, useUrlModal, useUrlDrawer, useUrlStepper and useUrlAccordion.

✏️ Partial Updates with PATCH

function TodoItem({ todo }: { todo: Todo }) {
  const { mutate: patchTodo } = todoApi.usePatch();
  
  // Only update the completed field
  const toggleCompleted = () => {
    patchTodo(todo.id, { completed: !todo.completed });
  };
  
  return (
    <div>
      <span>{todo.text}</span>
      <button onClick={toggleCompleted}>
        {todo.completed ? 'Mark Incomplete' : 'Mark Complete'}
      </button>
    </div>
  );
}

📡 Single Record APIs

import { createSingleRecordApi, createSingleRecordReadOnlyApi } from '@skylabs-digital/react-proto-kit';

// Full CRUD for single record (settings, profile, etc.)
const settingsApi = createSingleRecordApi(
  'users/:userId/settings',
  settingsSchema,
  settingsInputSchema,
  { 
    allowReset: true,           // Enable useReset() for reset to defaults
    refetchInterval: 30000      // Auto-refresh every 30 seconds
  }
);

// Read-only for computed/aggregate data (stats, analytics)
const statsApi = createSingleRecordReadOnlyApi(
  'dashboard/stats',
  statsSchema,
  { refetchInterval: 60000 }
);

Usage in components:

function UserSettings({ userId }: { userId: string }) {
  const api = settingsApi.withParams({ userId });
  
  // Fetch single record (not a list)
  const { data: settings, loading, refetch } = api.useRecord();
  
  // Update entire record (PUT - no ID needed)
  const { mutate: updateSettings, loading: updating } = api.useUpdate();
  
  // Partial update (PATCH - no ID needed)
  const { mutate: patchSettings } = api.usePatch();
  
  // Reset to defaults (DELETE - optional, requires allowReset: true)
  const { mutate: resetSettings } = api.useReset();
  
  const handleSave = async (newSettings: SettingsInput) => {
    const res = await updateSettings(newSettings);
    if (!res.success) {
      // Show toast, keep the form open, etc.
      return;
    }
  };

  const toggleDarkMode = async () => {
    await patchSettings({ darkMode: !settings?.darkMode });
    // Fire-and-forget: the hook's `error` state will surface any failure
  };

  if (loading) return <Spinner />;
  
  return (
    <SettingsForm 
      settings={settings} 
      onSave={handleSave}
      onToggleDarkMode={toggleDarkMode}
      onReset={resetSettings}
    />
  );
}

// Read-only dashboard stats
function DashboardStats() {
  const { data: stats, loading } = statsApi.useRecord();
  
  if (loading) return <StatsSkeleton />;
  
  return (
    <div>
      <StatCard title="Total Users" value={stats?.totalUsers} />
      <StatCard title="Active Today" value={stats?.activeToday} />
    </div>
  );
}

Key differences from createDomainApi:

| Feature | createDomainApi | createSingleRecordApi | createSingleRecordReadOnlyApi | |---------|-------------------|-------------------------|--------------------------------| | useList | ✅ | ❌ | ❌ | | useById | ✅ | ❌ | ❌ | | useRecord | ❌ | ✅ | ✅ | | useCreate | ✅ | ❌ | ❌ | | useUpdate | ✅ (with ID) | ✅ (no ID) | ❌ | | usePatch | ✅ (with ID) | ✅ (no ID) | ❌ | | useDelete | ✅ | ❌ | ❌ | | useReset | ❌ | ✅ (optional) | ❌ | | refetchInterval | ❌ | ✅ | ✅ |

📖 Read-Only APIs

For endpoints where you only need to fetch data (no create/update/delete):

import { createReadOnlyApi } from '@skylabs-digital/react-proto-kit';

// Read-only list endpoint (e.g., public catalog, reference data)
const combosApi = createReadOnlyApi('/combos/active', comboSchema);

function ComboList() {
  const { data: combos, loading } = combosApi.useList();
  if (loading) return <div>Loading...</div>;
  return <ul>{combos?.map(c => <li key={c.id}>{c.name}</li>)}</ul>;
}

💡 Also available: createSingleRecordReadOnlyApi for single-record GET-only endpoints (stats, analytics).

🎭 Data Orchestrator

Manage multiple API calls in a single component with smart loading states. Choose between Hook (flexible) or HOC (declarative) patterns:

Hook Pattern

import { useDataOrchestrator } from '@skylabs-digital/react-proto-kit';

function Dashboard() {
  const { data, isLoading, isFetching, hasErrors, errors, retryAll } = useDataOrchestrator({
    required: {
      users: userApi.useList,
      products: productApi.useList,
    },
    optional: {
      stats: statsApi.useRecord,
    },
  });

  if (isLoading) return <FullPageLoader />;
  if (hasErrors) return <ErrorPage errors={errors} onRetry={retryAll} />;

  return (
    <div>
      {isFetching && <TopBarSpinner />}
      <h1>Users: {data.users!.length}</h1>
      <h1>Products: {data.products!.length}</h1>
    </div>
  );
}

HOC Pattern (with Refetch)

import { withDataOrchestrator } from '@skylabs-digital/react-proto-kit';

interface DashboardData {
  users: User[];
  products: Product[];
}

function DashboardContent({ users, products, orchestrator }: DashboardData & { orchestrator: any }) {
  return (
    <div>
      {/* Refresh all data */}
      <button onClick={orchestrator.retryAll} disabled={orchestrator.isFetching}>
        {orchestrator.isFetching ? 'Refreshing...' : 'Refresh All'}
      </button>
      
      <h1>Users: {users.length}</h1>
      
      {/* Refresh individual resource */}
      <button onClick={() => orchestrator.retry('products')}>Refresh Products</button>
      {orchestrator.loading.products && <Spinner />}
      
      <h1>Products: {products.length}</h1>
    </div>
  );
}

export const Dashboard = withDataOrchestrator<DashboardData>(DashboardContent, {
  hooks: {
    users: userApi.useList,
    products: productApi.useList,
  }
});

URL-Driven Data with Auto-Refetch

import { withDataOrchestrator, useUrlTabs, useUrlParam } from '@skylabs-digital/react-proto-kit';

interface TodoListData {
  todos: Todo[];
}

function TodoListContent({ todos, orchestrator }: TodoListData & { orchestrator: any }) {
  const [activeTab, setTab] = useUrlTabs('status', ['active', 'completed', 'archived'], 'active');

  return (
    <div>
      {/* Tab navigation updates URL */}
      <button onClick={() => setTab('active')}>Active</button>
      <button onClick={() => setTab('completed')}>Completed</button>
      <button onClick={() => setTab('archived')}>Archived</button>

      {/* Non-blocking refetch indicator */}
      {orchestrator.isFetching && <span>🔄 Refreshing...</span>}

      {/* List updates automatically when tab changes */}
      <ul>
        {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
      </ul>
    </div>
  );
}

const TodoListWithData = withDataOrchestrator<TodoListData>(TodoListContent, {
  hooks: {
    todos: () => {
      const [status] = useUrlParam('status'); // Reads ?status=active
      return todoApi.withQuery({ status: status || 'active' }).useList();
    },
  },
  options: {
    watchSearchParams: ['status'], // Auto-refetch when ?status= changes
    refetchBehavior: 'stale-while-revalidate', // Smooth transitions (default)
  },
});

How it works:

  1. User clicks "Completed" tab → URL updates to ?status=completed
  2. watchSearchParams detects change
  3. Hook re-executes with new status value
  4. stale-while-revalidate shows "Active" todos while loading "Completed"
  5. Smooth transition when new data arrives

Key Features:

  • isLoading: Blocks rendering during first load of required resources
  • isFetching: Shows non-blocking indicator for refetches
  • watchSearchParams: Auto-refetch when specified URL params change
  • refetchBehavior: 'stale-while-revalidate' (smooth) or 'blocking' (explicit)
  • Required vs Optional: Control which resources block rendering
  • Granular Retry: Retry individual resources or all at once
  • Orchestrator Prop: HOC injects refetch capabilities automatically
  • Type-Safe: Full TypeScript inference for all data

Refetch Behaviors:

  • stale-while-revalidate (default): Shows previous data while fetching new data. Perfect for tabs, filters, pagination.
  • blocking: Clears data and shows loading state. Use for critical updates where stale data is misleading.

See Data Orchestrator Documentation for complete examples.

💾 Local Storage Mode

Perfect for prototyping without a backend — your API calls work exactly the same:

function App() {
  return (
    <BrowserRouter>
      <ApiClientProvider connectorType="localStorage">
        <GlobalStateProvider>
          {/* Your app works exactly the same! */}
        </GlobalStateProvider>
      </ApiClientProvider>
    </BrowserRouter>
  );
}

🐛 Debug Logging

Enable detailed request/response logging during development:

import { configureDebugLogging } from '@skylabs-digital/react-proto-kit';

// Enable all debug logs
configureDebugLogging({ enabled: true });

// Disable in production
configureDebugLogging({ enabled: false });

🌱 Seed Data

Pre-populate your app with seed data for development and demos:

import { createDevSeedConfig, createFallbackSeedConfig } from '@skylabs-digital/react-proto-kit';

// Seed data only in development
const seedConfig = createDevSeedConfig([
  { text: 'Learn React Proto Kit', completed: false, priority: 'high' },
  { text: 'Build something awesome', completed: false, priority: 'medium' },
]);

// Or use fallback seed (only if collection is empty)
const fallbackSeed = createFallbackSeedConfig([
  { text: 'Default item', completed: false },
]);

💡 Also available: createInitSeedConfig, createEnvironmentSeedConfig, and generateMockData for advanced seeding scenarios.


📚 API Reference

createDomainApi(path, entitySchema, upsertSchema, config?)

Creates a complete CRUD API for a resource.

Parameters:

  • path: string - Resource path (e.g., 'users', 'todos/:todoId/comments')
  • entitySchema: ZodSchema - Schema for entity responses
  • upsertSchema: ZodSchema - Schema for create/update operations
  • config?: object - Optional configuration

Config Options:

{
  cacheTime?: number;         // Cache duration in milliseconds
  queryParams?: {
    static?: Record<string, any>;   // Always included parameters
    dynamic?: string[];             // Runtime configurable parameters
  };
}

Returns: API object with methods:

  • useList(params?) - Fetch list of entities
  • useById(id) - Fetch single entity
  • useCreate() - Create new entity
  • useUpdate() - Update entire entity (PUT)
  • usePatch() - Partial update (PATCH)
  • useDelete() - Delete entity
  • withParams(params) - Inject path parameters (builder pattern)
  • withQuery(params) - Inject query parameters (builder pattern); propagates to mutations too

createSingleRecordApi(path, entitySchema, upsertSchema, config?)

Creates an API for single-record endpoints (settings, config, profile).

Parameters:

  • path: string - Resource path (e.g., 'settings', 'users/:userId/profile')
  • entitySchema: ZodSchema - Schema for entity responses
  • upsertSchema: ZodSchema - Schema for update operations
  • config?: object - Optional configuration

Config Options:

{
  cacheTime?: number;           // Cache duration in milliseconds
  refetchInterval?: number;     // Auto-refetch interval (for real-time data)
  allowReset?: boolean;         // Enable useReset() method
  queryParams?: {
    static?: Record<string, any>;
    dynamic?: string[];
  };
}

Returns: API object with methods:

  • useRecord() - Fetch single record
  • useUpdate() - Update entire record (PUT)
  • usePatch() - Partial update (PATCH)
  • useReset() - Reset to defaults (DELETE) - only if allowReset: true
  • withParams(params) - Inject path parameters
  • withQuery(params) - Inject query parameters

createSingleRecordReadOnlyApi(path, entitySchema, config?)

Creates a read-only API for computed/aggregate endpoints (stats, analytics).

Parameters:

  • path: string - Resource path (e.g., 'dashboard/stats')
  • entitySchema: ZodSchema - Schema for entity responses
  • config?: object - Optional configuration (same as above, minus allowReset)

Returns: API object with methods:

  • useRecord() - Fetch single record
  • withParams(params) - Inject path parameters
  • withQuery(params) - Inject query parameters

createReadOnlyApi(path, entitySchema)

Creates a read-only list API (no create/update/delete).

Returns: { useList, useById, withParams, withQuery }

Hooks

All hooks return objects with consistent interfaces:

Query Hooks (useList, useById, useRecord):

{
  data: T | T[] | null;
  loading: boolean;
  error: ErrorResponse | null;
  refetch: () => Promise<void>;
}

Mutation Hooks (useCreate, useUpdate, usePatch, useDelete):

{
  mutate: (...args) => Promise<ApiResponse<T>>;
  loading: boolean;
  error: ErrorResponse | null;  // 🛡️ Mirrors the last ErrorResponse, useful for persistent banners
}

💡 Mutations always resolve to an ApiResponse<T> and never throw. Check res.success before reading res.data. The error state on the hook is kept in sync for rendering persistent banners, but inline reactions belong to the awaited return value:

const res = await updateTodo.mutate(id, data);
if (!res.success) {
  showSnackbar({ message: res.message, variant: 'error' });
  return;
}
// res.data is the updated entity

This replaces the previous try/catch pattern — reading mutation.error immediately after await mutate(...) would return the stale value from the previous render, so returning the response from mutate is the only reliable way to react inline.

Type Utilities

| Utility | Description | |---------|-------------| | ExtractEntityType<T> | Complete entity type with auto-generated fields (id, createdAt, updatedAt) | | ExtractInputType<T> | Input type for create/update operations (excludes auto-generated fields) | | InferType<T> | Infer entity type from a Zod schema | | InferCreateType<T> | Infer create input type | | InferUpdateType<T> | Infer update input type |

📁 Examples

The repository includes 9 working examples covering different use cases:

| Example | Description | |---------|-------------| | 📝 Todo (Basic) | Simple CRUD operations with local state | | 📝 Todo (Global State) | Real-time state sync across components | | 📝 Todo (Backend) | Full-stack integration with Express.js | | 📝 Todo (Tabs) | URL-driven tabs with auto-refetch | | 📰 Blog (Backend) | Nested resources (posts → comments) | | 📰 Blog (Global State) | Blog with centralized state | | 📰 Blog (Basic) | Blog without global state | | 🔍 URL Navigation Demo | Modal, Drawer, Tabs, Stepper, Accordion | | 🐛 Debug Renders | Performance debugging and render tracking |

Run any example locally:

git clone https://github.com/skylabs-digital/react-proto-kit.git
cd react-proto-kit/examples/todo-with-backend
npm install
npm run dev

🎨 UI Components

Snackbar Notifications

Built-in toast-style notifications with auto-dismiss and queue management:

import { SnackbarProvider, SnackbarContainer, useSnackbar } from '@skylabs-digital/react-proto-kit';

// Setup (once in your app)
function App() {
  return (
    <SnackbarProvider>
      <SnackbarContainer position="top-right" maxVisible={3} />
      <YourApp />
    </SnackbarProvider>
  );
}

// Use in any component
function SaveButton() {
  const { showSnackbar } = useSnackbar();
  const { mutate: updateTodo } = todoApi.useUpdate();

  const handleSave = async () => {
    const res = await updateTodo('todo-1', { text: 'Updated', completed: false });
    if (!res.success) {
      showSnackbar({
        message: res.message ?? 'Error saving changes',
        variant: 'error',
        duration: 5000,
      });
      return;
    }
    showSnackbar({
      message: 'Changes saved successfully!',
      variant: 'success',
      duration: 3000,
    });
  };

  return <button onClick={handleSave}>Save</button>;
}

Snackbar Features:

  • ✅ 4 variants: success, error, warning, info
  • ✅ Auto-dismiss with configurable timeout
  • ✅ Queue system for multiple notifications
  • ✅ Optional action buttons (undo, etc.)
  • ✅ Fully customizable via SnackbarComponent prop
  • ✅ 6 position options (top/bottom, left/center/right)
  • ✅ Portal rendering for proper z-index

Custom Snackbar Component:

import { SnackbarItemProps } from '@skylabs-digital/react-proto-kit';

function CustomSnackbar({ snackbar, onClose, animate }: SnackbarItemProps) {
  return (
    <div style={{ /* your custom styles */ }}>
      <span>{snackbar.message}</span>
      {snackbar.action && (
        <button onClick={() => {
          snackbar.action.onClick();
          onClose(snackbar.id);
        }}>
          {snackbar.action.label}
        </button>
      )}
      <button onClick={() => onClose(snackbar.id)}>×</button>
    </div>
  );
}

// Use custom component
<SnackbarContainer SnackbarComponent={CustomSnackbar} />

Integration with CRUD APIs:

const todosApi = createDomainApi('todos', todoSchema);

function CreateTodoButton() {
  const { showSnackbar } = useSnackbar();
  const { mutate: createTodo, loading } = todosApi.useCreate();

  const onClick = async () => {
    const res = await createTodo({ text: 'New todo', completed: false });
    if (!res.success) {
      showSnackbar({ message: res.message ?? 'Create failed', variant: 'error' });
      return;
    }
    showSnackbar({ message: 'Todo created!', variant: 'success' });
  };

  return <button onClick={onClick} disabled={loading}>Add</button>;
}

Mutations return Promise<ApiResponse<T>> and never throw — there are no onSuccess/onError callback props. Handle outcomes inline after await mutate(...).

📖 Documentation

Comprehensive documentation is available in the docs/ directory:

| Guide | Description | |-------|-------------| | 📘 API Reference | Complete API documentation with all hooks and components | | 🛡️ Error Handling | Structured ErrorResponse, custom error codes, data field | | 🎨 UI Components | Modal, Drawer, Tabs, Stepper, Accordion, Snackbar | | 🎭 Data Orchestrator | Aggregate multiple APIs, stale-while-revalidate | | 📝 Forms Guide | Form handling, validation, useFormData, createFormHandler | | ⚡ Global Context | State management and real-time sync | | 🚀 Advanced Usage | Complex patterns and best practices | | 🏗️ Architecture | Internal design decisions and data flow | | 📡 Single Record API (RFC) | Design spec for single-record endpoints | | 🔄 Data Orchestrator Refetch (RFC) | Design spec for refetch behaviors | | 🔗 URL Navigation (RFC) | Design spec for URL-driven UI components | | 📋 Migration Guide | Upgrading between versions | | 🤝 Contributing | How to contribute to the project |

🛠 Backend Integration

Express.js Example

const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors());
app.use(express.json());

let todos = [];
let nextId = 1;

// GET /todos
app.get('/todos', (req, res) => {
  res.json(todos);
});

// POST /todos
app.post('/todos', (req, res) => {
  const todo = {
    id: String(nextId++),
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
    ...req.body
  };
  todos.push(todo);
  res.status(201).json(todo);
});

// PUT /todos/:id
app.put('/todos/:id', (req, res) => {
  const index = todos.findIndex(t => t.id === req.params.id);
  if (index === -1) return res.status(404).json({ error: 'Todo not found' });
  
  todos[index] = {
    ...todos[index],
    ...req.body,
    updatedAt: new Date().toISOString()
  };
  res.json(todos[index]);
});

// PATCH /todos/:id
app.patch('/todos/:id', (req, res) => {
  const index = todos.findIndex(t => t.id === req.params.id);
  if (index === -1) return res.status(404).json({ error: 'Todo not found' });
  
  todos[index] = {
    ...todos[index],
    ...req.body,
    updatedAt: new Date().toISOString()
  };
  res.json(todos[index]);
});

// DELETE /todos/:id
app.delete('/todos/:id', (req, res) => {
  const index = todos.findIndex(t => t.id === req.params.id);
  if (index === -1) return res.status(404).json({ error: 'Todo not found' });
  
  todos.splice(index, 1);
  res.status(204).send();
});

app.listen(3001, () => {
  console.log('Server running on http://localhost:3001');
});

🤝 Contributing

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

Development Setup

git clone https://github.com/skylabs-digital/react-proto-kit.git
cd react-proto-kit
npm install
npm run dev

Running Tests

yarn test              # Run tests once
yarn test --watch      # Run tests in watch mode
yarn ci                # Lint + type-check + test + build

📄 License

MIT © Skylabs Digital


Built with ❤️ by the Skylabs Digital team

Zod · React · Vitest · Vite

Ready to prototype at lightning speed?

Get Started · Explore Examples · Read the Docs