@periodic/strontium-react
v1.0.0
Published
Production-grade React integration for @periodic/strontium — hooks-first, Suspense-ready, and framework-pure
Maintainers
Readme
⚛️ Periodic Strontium React
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
fetchinuseEffecthas 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
unknowncasts
Periodic Strontium React provides the perfect solution:
✅ Zero extra dependencies — requires only @periodic/strontium and React
✅ Hooks-first — useStrontiumQuery 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/strontiumOr 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>
StrontiumProvideris 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 unmountuseStrontiumMutation— for writing data, fires only whenexecuteis called- Both hooks surface the same status lifecycle:
idle → loading → success | error - Both hooks expose typed errors — no
unknownin 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 APIDesign 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 unmount —
AbortControllersignals 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:watchNote: All tests achieve >80% code coverage.
🤝 Related Packages
Part of the Periodic series by Uday Thakur:
- @periodic/strontium - The core HTTP client (required peer dependency)
- @periodic/iridium - Structured logging
- @periodic/arsenic - Semantic runtime monitoring
- @periodic/zirconium - Environment configuration
- @periodic/vanadium - Idempotency and distributed locks
- @periodic/obsidian - HTTP error handling
- @periodic/titanium - Rate limiting
- @periodic/osmium - Redis caching
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
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
🌟 Show Your Support
Give a ⭐️ if this project helped you build better applications!
Built with ❤️ by Uday Thakur for production-grade Node.js applications
