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

@periodic/strontium-react

v1.0.0

Published

Production-grade React integration for @periodic/strontium — hooks-first, Suspense-ready, and framework-pure

Readme

⚛️ Periodic Strontium React

npm version License: MIT TypeScript

Production-grade React.js integration for @periodic/strontium — hooks-first, Suspense-ready, and framework-pure.

Part of the Periodic series of Node.js packages by Uday Thakur.


💡 Why Strontium React?

@periodic/strontium-react is the React binding layer for @periodic/strontium. It brings all of Strontium's resilience primitives — retry, circuit breaking, deduplication, and timeout control — directly into your React component tree via idiomatic hooks.

Most React data-fetching solutions come bundled with their own HTTP client, their own cache, their own global store, and their own opinions about how your app should be structured. Strontium React does none of that. It is a thin, deterministic bridge between Strontium's battle-tested request engine and React's component model — nothing more, nothing less.

The name represents:

  • Composability: Hooks that compose cleanly with the rest of your React architecture
  • Determinism: Request lifecycle follows the same strict state machine as the core client
  • Minimalism: No cache layer, no global store, no UI — just the integration
  • Safety: StrictMode-safe, SSR-safe, cancels on unmount by default

Just as @periodic/strontium makes your HTTP layer reliable, @periodic/strontium-react makes that reliability available everywhere in your component tree.


🎯 Why Choose Strontium React?

React applications make HTTP requests — and most solutions either bring too much or too little:

  • Raw fetch in useEffect has no retry, no cancellation on unmount, and race condition bugs
  • React Query / SWR are full solutions with their own HTTP layer, cache, and devtools — overkill if you already have Strontium
  • Axios + custom hooks work but scatter resilience logic across your codebase inconsistently
  • No Suspense support in hand-rolled hooks means loading states are always manual
  • No typed errors means catch blocks are full of unknown casts

Periodic Strontium React provides the perfect solution:

Zero extra dependencies — requires only @periodic/strontium and React
Hooks-firstuseStrontiumQuery and useStrontiumMutation cover every use case
Suspense-ready — throw promises while loading, throw errors for boundaries
SSR-safe — no window usage, defers browser-only behavior to client
StrictMode-safe — handles double invocation without duplicate requests
Cancels on unmount — no state updates after component is gone
Optimistic updates — built into the mutation hook
Typed errors — every error class from @periodic/strontium flows through
Tree-shakeable — import only what you use
No global store — pure React state, no external dependencies
No UI components — bring your own
Production-ready — non-blocking, never crashes your app


📦 Installation

npm install @periodic/strontium-react @periodic/strontium

Or with yarn:

yarn add @periodic/strontium-react @periodic/strontium

🚀 Quick Start

import { createStrontiumClient } from '@periodic/strontium';
import {
  StrontiumProvider,
  useStrontiumQuery,
  useStrontiumMutation,
} from '@periodic/strontium-react';

// 1. Create a Strontium client
const client = createStrontiumClient({
  baseURL: 'https://api.example.com',
  retry: { enabled: true, maxAttempts: 3, strategy: 'exponential' },
});

// 2. Wrap your app with the provider
function App() {
  return (
    <StrontiumProvider client={client}>
      <UserList />
    </StrontiumProvider>
  );
}

// 3. Fetch data with useStrontiumQuery
function UserList() {
  const { data, isLoading, isError, retry } = useStrontiumQuery<User[]>({
    key: 'users',
    request: { method: 'GET', url: '/users' },
  });

  if (isLoading) return <Spinner />;
  if (isError) return <button onClick={retry}>Retry</button>;
  return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

// 4. Mutate with useStrontiumMutation
function CreateUser() {
  const { execute, isLoading } = useStrontiumMutation<{ name: string }, { id: string }>({
    request: (body) => ({ method: 'POST', url: '/users', body }),
  });

  return (
    <button onClick={() => execute({ name: 'Alice' })} disabled={isLoading}>
      Create User
    </button>
  );
}

🧠 Core Concepts

The <StrontiumProvider>

  • StrontiumProvider is the root of the integration
  • Provides the Strontium client to all child hooks via React context
  • Create one client per upstream service and nest providers if needed
  • This is the only setup required — no stores, no reducers, no config files
const client = createStrontiumClient({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  retry: { enabled: true, maxAttempts: 3 },
  circuitBreaker: { failureThreshold: 5 },
});

<StrontiumProvider client={client}>
  <App />
</StrontiumProvider>

The Hook Pattern

  • useStrontiumQuery — for reading data, fires on mount, cancels on unmount
  • useStrontiumMutation — for writing data, fires only when execute is called
  • Both hooks surface the same status lifecycle: idle → loading → success | error
  • Both hooks expose typed errors — no unknown in catch blocks

Design principle:

The hooks are thin. All resilience logic — retry, circuit breaking, deduplication — lives in the Strontium client, not in the hooks. The hooks just make the client's behaviour reactive.


✨ Features

🔍 useStrontiumQuery

Data fetching with automatic execution on mount, deduplication, retry, and cancellation on unmount:

const { data, error, status, isLoading, isSuccess, isError, retry } =
  useStrontiumQuery<User[]>({
    key: 'users',
    request: { method: 'GET', url: '/users' },
    enabled: true,    // set to false to skip fetching
    suspense: false,  // set to true for Suspense mode
  });

✏️ useStrontiumMutation

Mutations with optimistic update support and typed callbacks:

const { execute, data, error, status, isLoading, isSuccess, isError, reset } =
  useStrontiumMutation<{ name: string }, { id: string }>({
    request: (body) => ({ method: 'POST', url: '/users', body }),
    onSuccess: (res) => console.log('Created:', res.data.id),
    onError: (err) => console.error('Failed:', err.message),
  });

await execute({ name: 'Alice' });

⏸️ Conditional Fetching

Skip a query until a condition is met:

const { data } = useStrontiumQuery<User>({
  key: `user-${id}`,
  request: { method: 'GET', url: `/users/${id}` },
  enabled: !!id, // only fetch when id is available
});

🌀 Suspense Mode

Let React handle loading and error states declaratively:

function UserProfile({ id }: { id: string }) {
  const { data } = useStrontiumQuery<User>({
    key: `user-${id}`,
    request: { method: 'GET', url: `/users/${id}` },
    suspense: true,
  });

  return <h1>{data.name}</h1>; // data is never undefined here
}

<Suspense fallback={<Loading />}>
  <ErrorBoundary fallback={<ErrorMessage />}>
    <UserProfile id="123" />
  </ErrorBoundary>
</Suspense>

📚 Common Patterns

1. Basic Data Fetching

function ProductList() {
  const { data, isLoading, isError, retry } = useStrontiumQuery<Product[]>({
    key: 'products',
    request: { method: 'GET', url: '/products' },
  });

  if (isLoading) return <Spinner />;
  if (isError) return <button onClick={retry}>Try Again</button>;
  return <ul>{data?.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

2. Form Submission

function CreateOrderForm() {
  const { execute, isLoading, isSuccess, isError, error } = useStrontiumMutation<
    OrderInput,
    Order
  >({
    request: (body) => ({ method: 'POST', url: '/orders', body }),
    onSuccess: (res) => router.push(`/orders/${res.data.id}`),
  });

  return (
    <form onSubmit={(e) => { e.preventDefault(); execute(formData); }}>
      {isError && <p>{error.message}</p>}
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Placing order...' : 'Place Order'}
      </button>
    </form>
  );
}

3. Optimistic Update

function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);

  const { execute } = useStrontiumMutation({
    request: () => ({ method: 'POST', url: `/posts/${postId}/like` }),
    onError: () => setLiked(false), // roll back on failure
  });

  return (
    <button onClick={() => { setLiked(true); execute(); }}>
      {liked ? '❤️' : '🤍'}
    </button>
  );
}

4. Multiple Services

const userClient = createStrontiumClient({ baseURL: process.env.USER_SERVICE_URL });
const orderClient = createStrontiumClient({ baseURL: process.env.ORDER_SERVICE_URL });

function App() {
  return (
    <StrontiumProvider client={userClient}>
      <StrontiumProvider client={orderClient}>
        <Dashboard />
      </StrontiumProvider>
    </StrontiumProvider>
  );
}

5. Severity-Based Error Handling

import { CircuitOpenError, RetryExhaustedError, TimeoutError } from '@periodic/strontium';

function DataWidget() {
  const { data, error, isError } = useStrontiumQuery({ key: 'data', request: { method: 'GET', url: '/data' } });

  if (isError) {
    if (error instanceof CircuitOpenError) return <ServiceDownBanner />;
    if (error instanceof RetryExhaustedError) return <RetryExhaustedMessage />;
    if (error instanceof TimeoutError) return <SlowNetworkWarning />;
    return <GenericError />;
  }

  return <DataView data={data} />;
}

6. With Zod Schema Validation

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

function UserCard({ id }: { id: string }) {
  const { data } = useStrontiumQuery({
    key: `user-${id}`,
    request: {
      method: 'GET',
      url: `/users/${id}`,
      schema: UserSchema,
    },
  });

  return <div>{data?.name}</div>;
}

7. Structured Logging Integration

import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';

const logger = createLogger({
  transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});

const client = createStrontiumClient({ baseURL: '...' });

client.use({
  onAfterResponse: (ctx, res) => logger.info('http.response', { url: ctx.url, status: res.status }),
  onError: (ctx, err) => logger.error('http.error', { url: ctx.url, error: err.message }),
});

function App() {
  return <StrontiumProvider client={client}><Routes /></StrontiumProvider>;
}

8. Production Configuration

import { createStrontiumClient } from '@periodic/strontium';
import { StrontiumProvider } from '@periodic/strontium-react';

const client = createStrontiumClient({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  timeoutMs: 8_000,
  retry: {
    enabled: true,
    maxAttempts: 3,
    strategy: 'exponential',
    baseDelayMs: 100,
    maxDelayMs: 5_000,
    jitter: true,
    retryOn: ['network', '5xx'],
  },
  circuitBreaker: {
    failureThreshold: 5,
    resetTimeoutMs: 30_000,
  },
  dedupe: true,
});

export function Providers({ children }: { children: React.ReactNode }) {
  return <StrontiumProvider client={client}>{children}</StrontiumProvider>;
}

🎛️ Configuration Options

useStrontiumQuery Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | key | string | required | Unique key for deduplication and identification | | request | RequestOptions | required | Strontium request configuration | | enabled | boolean | true | Set to false to skip the fetch | | suspense | boolean | false | Throw a promise for React Suspense |

useStrontiumQuery Return Value

| Field | Type | Description | |-------|------|-------------| | data | T \| undefined | Response data | | error | StrontiumError \| null | Typed error if request failed | | status | 'idle' \| 'loading' \| 'success' \| 'error' | Current request status | | isLoading | boolean | true while fetching | | isSuccess | boolean | true when data is available | | isError | boolean | true when an error occurred | | retry | () => void | Re-trigger the request |

useStrontiumMutation Options

| Option | Type | Description | |--------|------|-------------| | request | (input: TInput) => RequestOptions | Request factory function | | onSuccess | (res: Response<TOutput>) => void | Called on success | | onError | (err: StrontiumError) => void | Called on error |

useStrontiumMutation Return Value

| Field | Type | Description | |-------|------|-------------| | execute | (input: TInput) => Promise<void> | Trigger the mutation | | data | TOutput \| undefined | Response data | | error | StrontiumError \| null | Typed error if request failed | | status | 'idle' \| 'loading' \| 'success' \| 'error' | Current mutation status | | isLoading | boolean | true while mutating | | isSuccess | boolean | true when mutation succeeded | | isError | boolean | true when mutation failed | | reset | () => void | Reset status back to idle |


📋 API Reference

Components

<StrontiumProvider client={StrontiumClient}>

Hooks

useStrontiumQuery<T>(options: QueryOptions<T>): QueryResult<T>
useStrontiumMutation<TInput, TOutput>(options: MutationOptions<TInput, TOutput>): MutationResult<TInput, TOutput>

Types

import type {
  QueryOptions,
  QueryResult,
  MutationOptions,
  MutationResult,
} from '@periodic/strontium-react';

🧩 Architecture

@periodic/strontium-react/
├── src/
│   ├── context/               # React context and provider
│   │   └── index.tsx         # StrontiumProvider + useStrontiumClient
│   ├── hooks/                 # React hooks
│   │   ├── useStrontiumQuery.ts      # Query hook
│   │   └── useStrontiumMutation.ts   # Mutation hook
│   ├── suspense/              # Suspense integration
│   │   └── index.ts          # Promise throwing and cache
│   ├── types.ts               # TypeScript interfaces
│   └── index.ts               # Public API

Design Philosophy:

  • Hooks are thin — all resilience logic lives in @periodic/strontium, not here
  • No cache layer — deduplication is handled by the core client
  • No global store — pure useState, useCallback, useRef
  • SSR-safe — no browser APIs accessed at module load time
  • StrictMode-safe — double invocation is handled correctly throughout

📈 Performance

  • Deduplication — concurrent identical queries share a single in-flight request, handled by the core client
  • Cancellation on unmountAbortController signals prevent state updates on unmounted components
  • No re-renders on unrelated state — hooks are scoped to their own state, no global subscription overhead
  • Tree-shakeable — unused hooks are excluded from your bundle
  • No monkey-patching — clean React patterns only

🚫 Explicit Non-Goals

This package intentionally does not include:

❌ Its own HTTP client (use @periodic/strontium)
❌ A cache layer (use the core client's deduplication)
❌ Polling (use setInterval + retry)
❌ A global store or devtools
❌ UI components — bring your own
❌ Pagination helpers — compose with your own logic
❌ Magic or implicit behavior on import

Focus on doing one thing well: idiomatic React bindings for @periodic/strontium.


🎨 TypeScript Support

Full TypeScript support with complete type inference:

import type {
  QueryOptions,
  QueryResult,
  MutationOptions,
  MutationResult,
} from '@periodic/strontium-react';

// Fully typed — generics flow through automatically
const { data } = useStrontiumQuery<User[]>({
  key: 'users',
  request: { method: 'GET', url: '/users' },
});

data; // User[] | undefined

// Mutation input and output are both typed
const { execute } = useStrontiumMutation<{ name: string }, { id: string }>({
  request: (body) => ({ method: 'POST', url: '/users', body }),
});

🧪 Testing

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Run tests in watch mode
npm run test:watch

Note: All tests achieve >80% code coverage.


🤝 Related Packages

Part of the Periodic series by Uday Thakur:

Build complete, production-ready APIs with the Periodic series!


📖 Documentation


🛠️ Production Recommendations

SSR / Next.js

Strontium React is SSR-safe by design — no window access at module load time. Use it in Next.js App Router or Pages Router without any special configuration:

// app/providers.tsx
'use client';

import { createStrontiumClient } from '@periodic/strontium';
import { StrontiumProvider } from '@periodic/strontium-react';

const client = createStrontiumClient({ baseURL: process.env.NEXT_PUBLIC_API_URL });

export function Providers({ children }: { children: React.ReactNode }) {
  return <StrontiumProvider client={client}>{children}</StrontiumProvider>;
}

Error Monitoring

Integrate with error tracking at the client level — errors surface through the hooks automatically:

const client = createStrontiumClient({ baseURL: '...' });

client.use({
  onError: (ctx, err) => {
    Sentry.captureException(err, { extra: { url: ctx.url } });
  },
});

📝 License

MIT © Uday Thakur


🙏 Contributing

Contributions are welcome! Please read CONTRIBUTING.md for details on:

  • Code of conduct
  • Development setup
  • Pull request process
  • Coding standards
  • Architecture principles

📞 Support


🌟 Show Your Support

Give a ⭐️ if this project helped you build better applications!


Built with ❤️ by Uday Thakur for production-grade Node.js applications