npm package: @periodic/tungsten-react
Description: Official React integration for @periodic/tungsten-client — provider, hooks, and guards with minimal re-renders
Author: Uday Thakur
Latest version: 1.0.0

# ⚛️ Periodic Tungsten React

[![npm version](https://img.shields.io/npm/v/@periodic/tungsten-react.svg)](https://www.npmjs.com/package/@periodic/tungsten-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/)

**Official React integration for `@periodic/tungsten-client` — provider, hooks, and guards with minimal re-renders**

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

---

## 💡 Why Tungsten React?

**`@periodic/tungsten-react`** is the React integration layer for `@periodic/tungsten-client`. While `@periodic/tungsten-client` handles the browser authentication lifecycle — token storage, proactive refresh, multi-tab sync, and race protection — this library makes that lifecycle reactive: an `AuthProvider` that bridges the client's event system into React state, hooks that expose auth state and actions to any component in the tree, and a `RequireAuth` guard that handles redirects declaratively.

Most React auth integrations are hand-rolled: a `useEffect` that subscribes to some auth object, a context that re-renders the entire tree on every token refresh, and a protected route component that each developer reimplements slightly differently. **Tungsten React gives you one canonical pattern, subscribed at the right granularity, with no unnecessary re-renders.**

The name represents:
- **Reactivity**: Auth state changes propagate to components without polling
- **Composability**: `AuthProvider`, `useAuth`, `useAccessToken`, and `RequireAuth` compose cleanly
- **Minimalism**: Thin integration layer — all auth logic stays in `@periodic/tungsten-client`
- **Correctness**: SSR-safe, StrictMode-safe, and subscription-cleanup handled correctly

Just as `@periodic/tungsten-client` handles browser auth without magic, **@periodic/tungsten-react** handles React integration without reinventing the wheel.

---

## 🎯 Why Choose Tungsten React?

React authentication integrations get the subtle parts wrong more often than not:

- **Context re-renders the entire tree** on token refresh — every component that consumes auth context re-renders, even when only the token changed
- **No cleanup on unmount** — `useEffect` subscriptions leak, causing state updates on unmounted components
- **SSR errors** — accessing `localStorage` or `window` at render time crashes in SSR environments
- **StrictMode double-invocation** — effects run twice in development, causing double subscriptions or double logouts
- **No access token hook** — components that only need a token for API calls are forced to depend on the full auth context
- **Bespoke protected route implementations** — every team writes a slightly different `PrivateRoute` component

**Periodic Tungsten React** provides the perfect solution:

✅ **`AuthProvider`** — bridges `TungstenClient` events into React context with a single subscription  
✅ **`useAuth()`** — access full auth state, login, logout, and client from any component  
✅ **`useAccessToken()`** — access only the current token — no re-renders on unrelated state changes  
✅ **`useRequireAuth()`** — redirect unauthenticated users programmatically  
✅ **`RequireAuth`** — declarative guard component for protected routes  
✅ **Minimal re-renders** — components subscribe at the right granularity  
✅ **SSR-safe** — no `window` or `localStorage` access at render time  
✅ **StrictMode-safe** — subscription and cleanup handled correctly  
✅ **Typed** — all hooks and components are fully TypeScript-typed  
✅ **Zero opinions on routing** — works with React Router, TanStack Router, Next.js, or none  
✅ **No global state** — multiple `AuthProvider` instances are fully isolated  
✅ **Production-ready** — non-blocking, never crashes your app

---

## 📦 Installation

```bash
npm install @periodic/tungsten-react @periodic/tungsten-client react
```

Or with yarn:

```bash
yarn add @periodic/tungsten-react @periodic/tungsten-client react
```

---

## 🚀 Quick Start

```tsx
import { TungstenClient } from '@periodic/tungsten-client';
import { AuthProvider, useAuth, RequireAuth } from '@periodic/tungsten-react';

// 1. Create the client
const client = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
});

// 2. Wrap your app
function App() {
  return (
    <AuthProvider client={client}>
      <Router />
    </AuthProvider>
  );
}

// 3. Use auth state in any component
function Dashboard() {
  const { state, logout } = useAuth();

  if (state.status !== 'authenticated') {
    return <p>Please log in</p>;
  }

  return (
    <div>
      <h1>Welcome back</h1>
      <button onClick={logout}>Sign out</button>
    </div>
  );
}

// 4. Protect routes declaratively
function ProfilePage() {
  return (
    <RequireAuth redirectTo="/login">
      <Profile />
    </RequireAuth>
  );
}
```

**Auth state shape after login:**

```json
{
  "status": "authenticated",
  "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
  "expiresAt": 1708000900000,
  "authenticatedAt": 1708000000000
}
```

---

## 🧠 Core Concepts

### `AuthProvider`

- **The single integration point** between `TungstenClient` and React
- Subscribes to `client.on('stateChange')` once, on mount — one subscription for the entire tree
- Stores auth state in React context — components re-render only when state actually changes
- Cleans up the subscription on unmount — no leaks, safe in StrictMode
- Accepts the client instance as a prop — no global singleton, safe for SSR and testing

```tsx
const client = new TungstenClient({ refreshEndpoint: '/api/auth/refresh' });

<AuthProvider client={client}>
  <App />
</AuthProvider>
```

### `useAuth()`

- Returns the current `AuthStateData`, `login`, `logout`, and the `client` instance
- Re-renders the component whenever auth state transitions (login, logout, refresh, expiry)
- **Use this when your component cares about the full auth state** — login pages, nav bars, user menus

```tsx
const { state, login, logout, client } = useAuth();
```

### `useAccessToken()`

- Returns only the current access token string
- Re-renders only when the token itself changes (not on unrelated state transitions)
- **Use this in components that only need the token for API calls** — avoids re-renders on `refreshing` transitions

```tsx
const accessToken = useAccessToken();
```

### `RequireAuth`

- Renders children when authenticated, redirects when not
- **Design principle:** declarative guards belong in JSX, not in route configuration scattered across the app

```tsx
<RequireAuth redirectTo="/login">
  <ProtectedPage />
</RequireAuth>
```

---

## ✨ Features

### 🏠 AuthProvider

Bridge `TungstenClient` into React with a single subscription:

```tsx
import { TungstenClient } from '@periodic/tungsten-client';
import { AuthProvider } from '@periodic/tungsten-react';

const client = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  refreshThresholdMs: 60_000,
});

function Root() {
  return (
    <AuthProvider client={client}>
      <App />
    </AuthProvider>
  );
}
```

### 🪝 `useAuth()`

Full auth state and actions from any component:

```tsx
import { useAuth } from '@periodic/tungsten-react';

function NavBar() {
  const { state, logout } = useAuth();

  return (
    <nav>
      {state.status === 'authenticated' ? (
        <button onClick={logout}>Sign out</button>
      ) : (
        <a href="/login">Sign in</a>
      )}
    </nav>
  );
}
```

### 🔑 `useAccessToken()`

Access the current token without subscribing to the full state:

```tsx
import { useAccessToken } from '@periodic/tungsten-react';

function ApiComponent() {
  const accessToken = useAccessToken();

  async function fetchData() {
    const res = await fetch('/api/data', {
      headers: { Authorization: `Bearer ${accessToken}` },
    });
    return res.json();
  }
}
```

### 🛡️ `RequireAuth`

Declarative route protection:

```tsx
import { RequireAuth } from '@periodic/tungsten-react';

function App() {
  return (
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route
        path="/dashboard"
        element={
          <RequireAuth redirectTo="/login">
            <Dashboard />
          </RequireAuth>
        }
      />
    </Routes>
  );
}
```

### 🔄 `useRequireAuth()`

Programmatic redirect for components that need imperative auth checks:

```tsx
import { useRequireAuth } from '@periodic/tungsten-react';

function CheckoutPage() {
  useRequireAuth({ redirectTo: '/login', returnTo: '/checkout' });
  return <Checkout />;
}
```

---

## 📚 Common Patterns

### 1. Login Form

```tsx
import { useAuth } from '@periodic/tungsten-react';

function LoginForm() {
  const { login } = useAuth();

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    const form = e.target as HTMLFormElement;
    const email = (form.elements.namedItem('email') as HTMLInputElement).value;
    const password = (form.elements.namedItem('password') as HTMLInputElement).value;

    const res = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    if (res.ok) {
      const { accessToken, refreshToken } = await res.json();
      login({ accessToken, refreshToken }); // → state: authenticated
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" />
      <input name="password" type="password" />
      <button type="submit">Sign in</button>
    </form>
  );
}
```

### 2. Protected Route with React Router

```tsx
import { Routes, Route, Navigate } from 'react-router-dom';
import { RequireAuth } from '@periodic/tungsten-react';

function AppRoutes() {
  return (
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route
        path="/dashboard"
        element={<RequireAuth redirectTo="/login"><Dashboard /></RequireAuth>}
      />
      <Route
        path="/settings"
        element={<RequireAuth redirectTo="/login"><Settings /></RequireAuth>}
      />
    </Routes>
  );
}
```

### 3. Conditional UI Based on Auth State

```tsx
import { useAuth } from '@periodic/tungsten-react';
import type { AuthStateData } from '@periodic/tungsten-client';

function AuthStatus() {
  const { state } = useAuth();

  const statusLabel: Record<AuthStateData['status'], string> = {
    unauthenticated: 'Signed out',
    authenticating: 'Signing in...',
    authenticated: 'Signed in',
    refreshing: 'Refreshing session...',
    expired: 'Session expired',
    error: 'Auth error',
  };

  return <span>{statusLabel[state.status]}</span>;
}
```

### 4. Authenticated Data Fetching

```tsx
import { useAuth } from '@periodic/tungsten-react';
import { useEffect, useState } from 'react';

function UserProfile() {
  const { client, state } = useAuth();
  const [profile, setProfile] = useState(null);

  useEffect(() => {
    if (state.status !== 'authenticated') return;
    // client.fetch() handles token injection and refresh automatically
    client.fetch('/api/me').then(r => r.json()).then(setProfile);
  }, [client, state.status]);

  if (!profile) return <p>Loading...</p>;
  return <div>{profile.name}</div>;
}
```

### 5. Session Expiry Handling

```tsx
import { useAuth } from '@periodic/tungsten-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

function SessionWatcher() {
  const { state } = useAuth();
  const navigate = useNavigate();

  useEffect(() => {
    if (state.status === 'expired') {
      navigate('/login?reason=expired');
    }
  }, [state.status, navigate]);

  return null; // render anywhere in the tree
}
```

### 6. Loading State During Initial Hydration

```tsx
import { useAuth } from '@periodic/tungsten-react';

function AppShell({ children }: { children: React.ReactNode }) {
  const { state } = useAuth();

  // Show a loading screen while the client checks for an existing session
  if (state.status === 'authenticating') {
    return <LoadingScreen />;
  }

  return <>{children}</>;
}
```

### 7. Structured Logging Integration

```tsx
import { useEffect } from 'react';
import { useAuth } from '@periodic/tungsten-react';
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';

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

function AuthLogger() {
  const { state } = useAuth();

  useEffect(() => {
    logger.info('tungsten.react.state_change', { status: state.status });
  }, [state.status]);

  return null;
}
```

### 8. Production Setup

```tsx
import { TungstenClient } from '@periodic/tungsten-client';
import { AuthProvider } from '@periodic/tungsten-react';

const isDevelopment = process.env.NODE_ENV === 'development';

const client = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  storage: isDevelopment ? sessionStorage : localStorage,
  refreshThresholdMs: isDevelopment ? 5_000 : 60_000,
  onStateChange: (state) => {
    if (state.status === 'error') {
      Sentry.captureException(state.error);
    }
  },
});

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

---

## 🎛️ Configuration Options

### `AuthProvider` Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `client` | `TungstenClient` | required | The `TungstenClient` instance to bind |
| `children` | `React.ReactNode` | required | Child components |

### `RequireAuth` Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `React.ReactNode` | required | Content to render when authenticated |
| `redirectTo` | `string` | required | Path to redirect to when not authenticated |
| `returnTo` | `string` | — | Appended as `?returnTo=` query param on redirect |

### `useRequireAuth` Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `redirectTo` | `string` | required | Path to redirect to when not authenticated |
| `returnTo` | `string` | — | Appended as `?returnTo=` query param on redirect |

### `useAuth` Return Value

| Field | Type | Description |
|-------|------|-------------|
| `state` | `AuthStateData` | Current auth state (discriminated union) |
| `login` | `(tokens: AuthTokens) => void` | Login and store tokens |
| `logout` | `() => void` | Logout and clear tokens |
| `client` | `TungstenClient` | The underlying client instance |

---

## 📋 API Reference

### Components

```tsx
<AuthProvider client={TungstenClient} />
<RequireAuth redirectTo="/login" returnTo="/dashboard" />
```

### Hooks

```typescript
useAuth(): { state: AuthStateData; login: (tokens: AuthTokens) => void; logout: () => void; client: TungstenClient }
useAccessToken(): string | null
useRequireAuth(options: { redirectTo: string; returnTo?: string }): void
```

### Types

```typescript
import type { AuthProviderProps, RequireAuthProps } from '@periodic/tungsten-react';
import type { AuthStateData, AuthTokens } from '@periodic/tungsten-client';
```

---

## 🧩 Architecture

```
@periodic/tungsten-react/
├── src/
│   ├── AuthProvider.tsx       # AuthProvider component + useAuth, useAccessToken, useRequireAuth hooks
│   ├── RequireAuth.tsx        # RequireAuth guard component
│   ├── context.ts             # React context definition
│   ├── types.ts               # AuthProviderProps, RequireAuthProps
│   └── index.ts               # Public API
```

**Design Philosophy:**
- **One subscription** — `AuthProvider` subscribes once and owns the React state; hooks read from context
- **Thin layer** — all auth logic stays in `@periodic/tungsten-client`; this library only bridges events to React
- **No routing opinions** — `RequireAuth` accepts a `redirectTo` string and uses `window.location` by default; override for your router
- **No global state** — the client is passed as a prop; multiple providers in the same tree are fully isolated
- **StrictMode-safe** — the subscription in `useEffect` is idempotent and the cleanup is always correct

---

## 📈 Performance

- **One context re-render per state transition** — the entire tree only re-renders when auth state actually changes
- **`useAccessToken` re-renders less** — only updates when the token string changes, not on `refreshing` transitions
- **No polling** — state changes are event-driven via `TungstenClient.on('stateChange')`
- **No extra subscriptions** — all hooks read from a single context value, no additional listeners

---

## 🚫 Explicit Non-Goals

This package **intentionally does not** include:

❌ Browser token lifecycle management (use `@periodic/tungsten-client`)  
❌ Server-side token signing or verification (use `@periodic/tungsten`)  
❌ A built-in router integration — pass `redirectTo` and handle navigation yourself  
❌ A login form or any UI components  
❌ OAuth / OpenID Connect flows  
❌ Magic or implicit behavior on import  
❌ Configuration files (configure in code)

Focus on doing one thing well: **idiomatic React bindings for `@periodic/tungsten-client`**.

---

## 🎨 TypeScript Support

Full TypeScript support with complete type safety:

```typescript
import type { AuthProviderProps, RequireAuthProps } from '@periodic/tungsten-react';
import type { AuthStateData } from '@periodic/tungsten-client';

// useAuth returns a fully typed object
const { state } = useAuth();
if (state.status === 'authenticated') {
  state.accessToken; // string — only present in this branch
}

// RequireAuth props are typed
<RequireAuth redirectTo="/login" returnTo={location.pathname}>
  <ProtectedPage />
</RequireAuth>
```

---

## 🧪 Testing

```bash
# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Run tests in watch mode
npm run test:watch
```

Tests use `@testing-library/react` and a mock `TungstenClient`.

**Note:** All tests achieve >80% code coverage.

---

## 🤝 Related Packages

Part of the **Periodic** series by Uday Thakur:

- [**@periodic/tungsten-session**](https://www.npmjs.com/package/@periodic/tungsten-session) - Server-side session management
- [**@periodic/tungsten-client**](https://www.npmjs.com/package/@periodic/tungsten-client) - Browser token state management
- [**@periodic/tungsten**](https://www.npmjs.com/package/@periodic/tungsten) - Authentication primitives (JWT, Argon2, TOTP, HMAC)
- [**@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/strontium**](https://www.npmjs.com/package/@periodic/strontium) - Resilient HTTP client
- [**@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)
- [Installation Guide](INSTALLATION.md)
- [Contributing Guide](CONTRIBUTING.md)
- [Changelog](CHANGELOG.md)

---

## 🛠️ Production Recommendations

### Environment Variables

```bash
NODE_ENV=production
NEXT_PUBLIC_API_URL=https://api.example.com
```

### Log Aggregation

Pair with `@periodic/iridium` for structured JSON output:

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

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

// Pass onStateChange to the TungstenClient, not the AuthProvider
const client = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  onStateChange: (state) => {
    logger.info('tungsten.react.state_change', { status: state.status });
    if (state.status === 'error') {
      logger.error('tungsten.react.error', { error: state.error?.message });
    }
  },
});

// Pipe to Elasticsearch, Datadog, CloudWatch, etc.
```

### Error Monitoring

```typescript
const client = new TungstenClient({
  refreshEndpoint: '/api/auth/refresh',
  onStateChange: (state) => {
    if (state.status === 'error') {
      Sentry.captureException(state.error, { extra: { authStatus: state.status } });
    }
  },
});
```

---

## 📝 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-tungsten-react/issues)
- 💬 **Discussions:** [GitHub Discussions](https://github.com/udaythakur7469/periodic-tungsten-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**