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

reactscrew

v1.1.2

Published

React provider and hook for organising API modules as screws

Readme

reactscrew

A typed React data layer built around domain modules called screws.

Features

  • Typed hooksuseScrewQuery, useScrewMutation with full generic inference
  • Cache — In-memory cache with deduplication, invalidation, and versioned persistence
  • Multi-backend — Route queries/mutations to different APIs by screw name
  • Infinite queries — Cursor/page-based pagination out of the box
  • Hydration — SSR/SSG via dehydrate / hydrate
  • Observability — Devtools panel, request event stream, Sentry/OpenTelemetry integrations
  • Orchestration — Batch, workflow, and progress tracking across backends
  • Transport adaptersfetch and axios, with auth retry and proxy rewriting
  • OpenAPI generation — Generate screws, hooks, types, and validators from OpenAPI specs
  • Error contract — Unified ReactScrewError with code, status, retryable, uiHint
  • UX feedback — Toast notifications and loading indicators wired to request lifecycle
  • Legacy APIuseScrew for gradual migration

Installation

npm install reactscrew

react and react-dom (≥18) are peer dependencies.

Quick Start

import { createRoot } from 'react-dom/client';
import { DriverProvider, createFetchAdapter, useScrewQuery } from 'reactscrew';

const api = createFetchAdapter('https://jsonplaceholder.typicode.com');

const userScrew = {
  name: 'user',
  methods: {
    list: { type: 'query', route: '/users', httpMethod: 'GET' },
    create: {
      type: 'mutation',
      route: '/users',
      httpMethod: 'POST',
      invalidateQueries: [{ screwName: 'user', methodName: 'list' }]
    }
  }
};

function App() {
  const { data, isLoading } = useScrewQuery('user', 'list');
  if (isLoading) return <p>Loading...</p>;
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

createRoot(document.getElementById('root')).render(
  <DriverProvider apiInstance={api} screws={{ user: userScrew }}>
    <App />
  </DriverProvider>
);

1. Declaring a Screw

A screw is a domain module that declares its API endpoints, validators, cache configuration, and invalidation relationships.

Basic Structure

import type { ScrewDefinition } from 'reactscrew';

const productScrew: ScrewDefinition = {
  name: 'product',
  methods: {
    // ── Query (GET) ──
    list: {
      type: 'query',
      route: '/products',
      httpMethod: 'GET',
      staleTime: 30_000,          // 30s before stale
      cacheTime: 5 * 60_000,     // 5min before garbage collection
      refetchOnWindowFocus: true,
    },

    // ── Query with parameters ──
    get: {
      type: 'query',
      route: (params: { id: number }) => `/products/${params.id}`,
      httpMethod: 'GET',
      queryKey: ({ screwName, methodName, args }) =>
        [screwName, methodName, args[0] ?? null],
    },

    // ── Mutation (POST) with invalidation ──
    create: {
      type: 'mutation',
      route: '/products',
      httpMethod: 'POST',
      invalidateQueries: [
        { screwName: 'product', methodName: 'list' }
      ],
    },

    // ── Mutation (PATCH) with optimistic update ──
    update: {
      type: 'mutation',
      route: (params: { id: number }) => `/products/${params.id}`,
      httpMethod: 'PATCH',
      optimisticUpdate: async ({ client, variables }) => {
        await client.setQueryData(['product', 'get'], (prev) => ({
          ...prev, ...variables
        }));
        return { rollback: () => {} };
      },
    },
  },
};

Method Properties

| Property | Type | Description | |-----------|------|-------------| | type | 'query' \| 'mutation' | Inferred from httpMethod if omitted | | route | string \| (...args) => string | URL template | | httpMethod | 'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE' | HTTP verb | | staleTime | number (ms) | Duration before data is considered stale | | cacheTime | number (ms) | In-memory retention duration | | refetchOnWindowFocus | boolean | Refetch on window focus | | refetchOnReconnect | boolean | Refetch on network reconnection | | queryKey | (ctx) => QueryKey | Custom cache key | | invalidateQueries | QueryInvalidationTarget[] | Screws to invalidate after mutation | | optimisticUpdate | (ctx) => RollbackAction | Optimistic update logic | | onSuccess / onError / onSettled | function | Lifecycle callbacks | | headers | Record<string, string> | Custom headers | | paramsValidator / responseValidator | RuntimeValidator | Runtime validation | | documentedErrors | DocumentedErrorDefinition[] | Error catalog |

Screws are used with useScrewQuery / useScrewMutation:

const { data } = useScrewQuery('product', 'list');
const { mutate } = useScrewMutation('product', 'create');

2. Multi-Backend Management

reactscrew can route requests to multiple independent APIs from a single DriverProvider.

Configuration

import { DriverProvider, useScrewQuery, createFetchAdapter } from 'reactscrew';

const productsApi = createFetchAdapter('https://fakestoreapi.com');
const usersApi = createFetchAdapter('https://jsonplaceholder.typicode.com');

const backends = {
  products: {
    apiInstance: productsApi,
    screws: {
      product: {
        name: 'product',
        methods: {
          list: { type: 'query', route: '/products', httpMethod: 'GET' },
        },
      },
    },
  },
  users: {
    apiInstance: usersApi,
    screws: {
      user: {
        name: 'user',
        methods: {
          list: { type: 'query', route: '/users', httpMethod: 'GET' },
        },
      },
    },
  },
};

function App() {
  return (
    <DriverProvider backends={backends}>
      <ProductList />
      <UserList />
    </DriverProvider>
  );
}

Automatic Routing

useScrewQuery('product', 'list') searches for the product screw across all backends and routes to the correct client automatically.

// Automatic routing (by screw name)
const { data: products } = useScrewQuery('product', 'list');
const { data: users } = useScrewQuery('user', 'list');

// Explicit routing
const { data } = useScrewQuery('product', 'list', { backend: 'products' });

Isolation

Each backend has its own cache, its own metrics, and its own events. Data from one backend never leaks into another.

Mix legacy + backends

<DriverProvider
  apiInstance={defaultApi}
  screws={{ legacy: { name: 'legacy', methods: { ... } } }}
  backends={{
    billing: { apiInstance: billingApi, screws: { invoice: { ... } } },
  }}
/>

3. OpenAPI Generation

reactscrew can generate complete screws, TypeScript hooks, types, and validators from an OpenAPI 3.0/3.1 file.

Exposed API (browser-safe)

These functions are exported from the main barrel:

import {
  parseOpenApiDocument,          // Parse an OpenAPI document → contract
  validateOpenApiContract,       // Validate the parsed contract
  generateScrewsFromOpenApiContract,  // Generate screw definitions (string)
  generateOpenApiArtifacts,      // Generate all artifacts (types, hooks, validators, errors, screws)
  generateOpenApiArtifactsFromDocument, // Direct version (no file)
  loadOpenApiContract,           // (Node.js) Load a file or URL
} from 'reactscrew';

Generation from a file (Node.js)

npx reactscrew-openapi openapi.json ./output

Produces 9 files:

output/
├── index.ts
├── custom/index.ts          ← preserved on regeneration
├── wrappers/index.ts        ← preserved on regeneration
└── generated/
    ├── index.ts
    ├── types/index.ts       ← TypeScript interfaces
    ├── errors/index.ts      ← documented error catalog
    ├── validators/index.ts  ← runtime validators
    ├── screws/index.ts      ← screw definitions
    └── hooks/index.ts       ← typed hooks (useXQuery, useXMutation)

Example e-commerce spec

{
  "openapi": "3.0.0",
  "info": { "title": "Products API", "version": "1.0.0" },
  "paths": {
    "/products": {
      "get": {
        "operationId": "listProducts",
        "parameters": [
          { "name": "category", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Product" } }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Product": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "title": { "type": "string" },
          "price": { "type": "number" }
        }
      }
    }
  }
}

Generates:

// screws
const productsScrew = {
  name: "products",
  methods: {
    listProducts: {
      type: 'query',
      route: (params) => `/products?category=${params.category}`,
      httpMethod: 'GET',
      paramsValidator: validateListProductsParamsArgs,
      responseValidator: validateListProductsResponse,
    }
  }
};

// typed hook
const { data } = useListProductsQuery({ category: 'electronics' });

Available e-commerce specs

The examples/e-commerce/openapi/ directory contains 3 specs:

| File | Generated Screws | Endpoints | |---------|----------------|-----------| | users.openapi.json | users, auth, wishlist | 6 (CRUD users, login, wishlist) | | products.openapi.json | products, categories | 5 (catalog, categories) | | orders.openapi.json | cart, orders | 7 (cart, checkout, orders) |

Usage:

node scripts/generate-openapi-screws.mjs examples/e-commerce/openapi/products.openapi.json ./generated/products

4. Orchestration

reactscrew offers three orchestration levels: batch, workflow, and progress tracking.

Batch — Parallel action execution

executeBatch runs a list of mutation actions (potentially across multiple backends) and reports progress.

import { useScrewBatch } from 'reactscrew';

function CheckoutAll() {
  const { execute, progress, isExecuting } = useScrewBatch([
    { screwName: 'order', methodName: 'checkout', variables: { cartId: 1 }, backend: 'orders' },
    { screwName: 'inventory', methodName: 'reserve', variables: { productId: 5 }, backend: 'products' },
  ]);

  return (
    <button onClick={() => execute()} disabled={isExecuting}>
      {isExecuting ? `⏳ ${progress?.percentage}%` : '⚡ Order all'}
    </button>
  );
}

Workflow — Sequential orchestration with dependencies

executeWorkflow runs steps with dependencies, retry, parallel mode, and conditional gating.

import { executeWorkflow, useScrewWorkflow } from 'reactscrew';

function CheckoutWorkflow() {
  const { execute, progress } = useScrewWorkflow({
    steps: [
      { id: 'reserve',  screwName: 'inventory', methodName: 'reserve',  variables: { productId: 5 } },
      { id: 'payment',  screwName: 'billing',   methodName: 'charge',   variables: { amount: 99 }, dependsOn: ['reserve'] },
      { id: 'ship',     screwName: 'logistics', methodName: 'ship',     variables: { orderId: 1 },  dependsOn: ['payment'], retry: 2 },
      { id: 'notify',   screwName: 'notifications', methodName: 'email', variables: { userId: 1 },  dependsOn: ['ship'],   continueOnError: true },
    ],
  });

  return <button onClick={() => execute()}>🔁 Run workflow</button>;
}

Workflow-level conditions

A workflow can have a global condition evaluated before any step runs. When paired with waitForCondition: true, the workflow polls every 500ms and re-evaluates the condition until it passes (up to 30s).

const result = await executeWorkflow({
  steps: [
    { id: 'checkout', screwName: 'orders', methodName: 'checkout', variables: { shippingAddress } },
    { id: 'tracking', screwName: 'delivery', methodName: 'track', args: [{ orderId: 0 }], dependsOn: ['checkout'] },
  ],
  condition: (ctx) => {
    const cart = ctx.getScrewData('cart', 'get');
    return cart?.items?.length >= (ctx.variables?.minItems ?? 1);
  },
  waitForCondition: true,
  variables: { minItems: 1 },
  onStepCondition: (result) => {
    if (!result.passed) console.log(`⏳ Waiting for "${result.stepId}"`);
  },
}, { client, resolveClient, onProgress });
  • condition — receives { stepResults, getScrewData, variables }. Return true to proceed.
  • waitForCondition — if the condition returns false, the workflow re-polls.
  • variables — arbitrary data injected into the condition context via ctx.variables.
  • onStepCondition — callback when a step-level or workflow-level condition is evaluated.

Step-level conditions

Individual steps can also have conditions:

{
  id: 'apply-discount',
  screwName: 'cart',
  methodName: 'applyCoupon',
  condition: (ctx) => ctx.stepResults['checkout']?.data?.couponApplied === true,
  waitForCondition: true,
}

Declarative workflows in screw definitions

Workflows can be declared directly in a ScrewDefinition and auto-executed:

const cartScrew: ScrewDefinition = {
  name: 'cart',
  methods: { get: { type: 'query', route: '/cart', httpMethod: 'GET' } },
  workflows: {
    checkout: {
      config: {
        steps: [
          { id: 'validate', screwName: 'cart', methodName: 'get', label: 'Stock check' },
          { id: 'checkout', screwName: 'orders', methodName: 'checkout', dependsOn: ['validate'] },
        ],
        condition: (ctx) => {
          const cart = ctx.getScrewData('cart', 'get');
          return cart?.items?.length > 0;
        },
        waitForCondition: true,
        variables: { minItems: 1 },
      },
      autoStart: false,
    },
  },
};

Use useScrewAutoWorkflow to execute a declared workflow by passing its config:

const { start, progress, status } = useScrewAutoWorkflow({
  config: { steps, condition, waitForCondition, variables },
  onProgress: (snap) => setProgress(snap),
});

Progress Tracking

useScrewProgress derives a unified ProgressSnapshot from any batch or workflow source:

import { useScrewBatch, useScrewProgress } from 'reactscrew';

function ProgressBar() {
  const batch = useScrewBatch([...]);
  const progress = useScrewProgress(batch);

  if (!progress || progress.phase === 'idle') return null;

  return (
    <div>
      <progress value={progress.percentage} max={100} />
      <span>{progress.percentage}% · {progress.itemsProcessed}/{progress.itemsTotal}</span>
    </div>
  );
}

SSR / Server Components

All hooks and DriverProvider are marked 'use client'. They are safe for use in Next.js App Router client boundaries.

SSR data prefetching uses createReactScrewClient (exported from the package):

// app/page.tsx — Server Component
import { createFetchAdapter, createReactScrewClient } from 'reactscrew';
import { userScrew } from '../screws/user';

export default async function Page() {
  const client = createReactScrewClient(api, { user: userScrew });
  await client.prefetchQuery('user', 'list');
  const dehydratedState = client.dehydrate();

  return <UsersClient dehydratedState={dehydratedState} />;
}
// app/users-client.tsx — Client Component
'use client';
import { DriverProvider, useScrewQuery } from 'reactscrew';

export default function UsersClient({ dehydratedState }) {
  return (
    <DriverProvider apiInstance={api} screws={{ user: userScrew }}
      dehydratedState={dehydratedState}>
      <UsersList />
    </DriverProvider>
  );
}

Limits:

  • Hooks are client-only ('use client' boundary). No RSC-specific exports.
  • Cache persistence (persist option) uses localforage (IndexedDB). Do not enable in SSR; it will be safely ignored.
  • createReactScrewClient is pure and safe for server usage; use for prefetch in Server Components or Route Handlers.

Documentation

| Topic | Resource | |-------|----------| | Execution roadmap | TASK.md | | Vision & levels | evolving.md | | Full API reference | API_REFERENCE.md — every hook, component, function, and type | | Best practices | BEST_PRACTICES.md — cache strategy, configuration values, pitfalls | | TypeScript types | src/index.ts (JSDoc per export) | | Multi-backend e-commerce demo | examples/e-commerce | | OpenAPI 3.0 specs | examples/e-commerce/openapi/ | | Generated artifacts | examples/e-commerce/generated/ | | Examples | examples/basic, examples/openapi-generated, examples/next-app-router |

Performance

Benchmarks measured on Node.js v22.22.0, Linux, against a local loopback HTTP server (~2ms network latency per request). All figures are the average of 30–500 iterations.

| Benchmark | avg | p50 | p95 | |---|---|---|---| | Cold query (first fetch, ~2ms network) | 5.24 ms | 3.63 ms | 8.58 ms | | Warm query (from cache) | 3.28 ms | 3.25 ms | 3.81 ms | | Cold slow query (~30ms network) | 31.22 ms | 31.24 ms | 31.72 ms | | Mutation execution | 3.69 ms | 3.55 ms | 4.32 ms | | Concurrent dedup (5 identical requests, 1 network call) | 3.35 ms | — | — | | Batch (5 items, parallel) | 0.42 ms | — | — | | Workflow (3 sequential steps) | 0.21 ms | — | — | | OpenAPI parse (15-endpoint document) | 0.18 ms | 0.14 ms | 0.23 ms | | Logger.info (enabled, JSON output) | 0.037 ms | 0.022 ms | 0.030 ms | | Logger.debug (filtered by log level, ~0 cost) | 0.001 ms | — | — |

Bundle size: dist/=616 KB (38 files), gzipped=62.7 KB. Barrel re-export: 6.9 KB. Tree-shaking eliminates unused modules.

Cache overhead: ~0.5 KB per cache entry (based on 500 entries). Concurrent dedup routes identical requests through a single network call — subsequent subscribers await the same in-flight promise.

Scripts

npm run typecheck   # TypeScript strict check
npm run test        # Vitest (136 tests)
npm run lint        # ESLint
npm run build       # tsc → dist/
npm run demo        # Webpack dev server (e-commerce demo)
DEMO=basic npm run demo  # Basic demo alternative