@periodic/tungsten-react
v1.0.0
Published
Official React integration for @periodic/tungsten-client — provider, hooks, and guards with minimal re-renders
Downloads
12
Maintainers
Readme
⚛️ Periodic Tungsten React
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, andRequireAuthcompose 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 —
useEffectsubscriptions leak, causing state updates on unmounted components - SSR errors — accessing
localStorageorwindowat 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
PrivateRoutecomponent
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
npm install @periodic/tungsten-react @periodic/tungsten-client reactOr with yarn:
yarn add @periodic/tungsten-react @periodic/tungsten-client react🚀 Quick Start
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:
{
"status": "authenticated",
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"expiresAt": 1708000900000,
"authenticatedAt": 1708000000000
}🧠 Core Concepts
AuthProvider
- The single integration point between
TungstenClientand 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
const client = new TungstenClient({ refreshEndpoint: '/api/auth/refresh' });
<AuthProvider client={client}>
<App />
</AuthProvider>useAuth()
- Returns the current
AuthStateData,login,logout, and theclientinstance - 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
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
refreshingtransitions
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
<RequireAuth redirectTo="/login">
<ProtectedPage />
</RequireAuth>✨ Features
🏠 AuthProvider
Bridge TungstenClient into React with a single subscription:
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:
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:
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:
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:
import { useRequireAuth } from '@periodic/tungsten-react';
function CheckoutPage() {
useRequireAuth({ redirectTo: '/login', returnTo: '/checkout' });
return <Checkout />;
}📚 Common Patterns
1. Login Form
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
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
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
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
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
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
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
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
<AuthProvider client={TungstenClient} />
<RequireAuth redirectTo="/login" returnTo="/dashboard" />Hooks
useAuth(): { state: AuthStateData; login: (tokens: AuthTokens) => void; logout: () => void; client: TungstenClient }
useAccessToken(): string | null
useRequireAuth(options: { redirectTo: string; returnTo?: string }): voidTypes
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 APIDesign Philosophy:
- One subscription —
AuthProvidersubscribes 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 —
RequireAuthaccepts aredirectTostring and useswindow.locationby 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
useEffectis 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
useAccessTokenre-renders less — only updates when the token string changes, not onrefreshingtransitions- 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:
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
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watchTests 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 - Server-side session management
- @periodic/tungsten-client - Browser token state management
- @periodic/tungsten - Authentication primitives (JWT, Argon2, TOTP, HMAC)
- @periodic/iridium - Structured logging
- @periodic/arsenic - Semantic runtime monitoring
- @periodic/zirconium - Environment configuration
- @periodic/vanadium - Idempotency and distributed locks
- @periodic/strontium - Resilient HTTP client
- @periodic/titanium - Rate limiting
- @periodic/osmium - Redis caching
Build complete, production-ready APIs with the Periodic series!
📖 Documentation
🛠️ Production Recommendations
Environment Variables
NODE_ENV=production
NEXT_PUBLIC_API_URL=https://api.example.comLog Aggregation
Pair with @periodic/iridium for structured JSON output:
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
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
🙏 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
