npm package: @periodic/strontium-react
Description: Production-grade React integration for @periodic/strontium — hooks-first, Suspense-ready, and framework-pure
Author: Uday Thakur
Latest version: 1.0.0

# ⚛️ Periodic Strontium React

[![npm version](https://img.shields.io/npm/v/@periodic/strontium-react.svg)](https://www.npmjs.com/package/@periodic/strontium-react)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](https://www.typescriptlang.org/)

**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-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

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

Or with yarn:

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

---

## 🚀 Quick Start

```tsx
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

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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

```tsx
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

```tsx
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

```tsx
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

```tsx
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

```tsx
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

```tsx
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

```tsx
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

```tsx
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

```typescript
<StrontiumProvider client={StrontiumClient}>
```

### Hooks

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

### Types

```typescript
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 unmount** — `AbortController` 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:

```typescript
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

```bash
# 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:

- [**@periodic/strontium**](https://www.npmjs.com/package/@periodic/strontium) - The core HTTP client (required peer dependency)
- [**@periodic/iridium**](https://www.npmjs.com/package/@periodic/iridium) - Structured logging
- [**@periodic/arsenic**](https://www.npmjs.com/package/@periodic/arsenic) - Semantic runtime monitoring
- [**@periodic/zirconium**](https://www.npmjs.com/package/@periodic/zirconium) - Environment configuration
- [**@periodic/vanadium**](https://www.npmjs.com/package/@periodic/vanadium) - Idempotency and distributed locks
- [**@periodic/obsidian**](https://www.npmjs.com/package/@periodic/obsidian) - HTTP error handling
- [**@periodic/titanium**](https://www.npmjs.com/package/@periodic/titanium) - Rate limiting
- [**@periodic/osmium**](https://www.npmjs.com/package/@periodic/osmium) - Redis caching

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

---

## 📖 Documentation

- [Quick Start Guide](QUICKSTART.md)
- [Contributing Guide](CONTRIBUTING.md)
- [Changelog](CHANGELOG.md)

---

## 🛠️ 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:

```tsx
// 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:

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

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

---

## 📝 License

MIT © [Uday Thakur](LICENSE)

---

## 🙏 Contributing

Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on:

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

---

## 📞 Support

- 📧 **Email:** udaythakurwork@gmail.com
- 🐛 **Issues:** [GitHub Issues](https://github.com/udaythakur7469/periodic-strontium-react/issues)
- 💬 **Discussions:** [GitHub Discussions](https://github.com/udaythakur7469/periodic-strontium-react/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**