Client-Side Usage
The @ops-ai/nextjs-toggly-client package provides React hooks and components for Client Components in Next.js.
Installation
npm install @ops-ai/nextjs-toggly-core @ops-ai/nextjs-toggly-client
Setup
Provider Setup
Wrap your application with the TogglyProvider:
'use client'
import { TogglyProvider } from '@ops-ai/nextjs-toggly-client'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<TogglyProvider
config={{
appKey: process.env.NEXT_PUBLIC_TOGGLY_APP_KEY!,
environment: 'Production',
onError: (message, error) => {
// Report fetch/cache/refresh failures to your monitoring provider
console.warn('Toggly error:', message, error)
},
}}
autoInit={true}
>
{children}
</TogglyProvider>
)
}
import { Providers } from './providers'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
Server-Side Hydration
For optimal performance, pass server-rendered features to the client:
import { initServerToggly, getServerToggly } from '@ops-ai/nextjs-toggly-server'
import { Providers } from './providers'
export default async function RootLayout({ children }) {
await initServerToggly({
appKey: process.env.TOGGLY_APP_KEY!,
})
const toggly = getServerToggly()
const features = toggly?.state.features ?? {}
return (
<html>
<body>
<Providers initialFeatures={features}>{children}</Providers>
</body>
</html>
)
}
Error reporting and refresh behavior
Pass onError in the provider config to observe fetch, cache, parse, WebSocket,
and refresh failures:
'use client'
<TogglyProvider
config={{
appKey: process.env.NEXT_PUBLIC_TOGGLY_APP_KEY!,
environment: 'Production',
onError: (message, error) => {
monitoring.captureException(error, {
tags: { source: 'toggly' },
extra: { message }
});
},
}}
>
{children}
</TogglyProvider>
useToggly() exposes error from the underlying client state. After one
successful load, refresh failures preserve the last-known-good flags instead of
replacing rendered content with defaults or empty flags. TogglyProvider,
hooks, and components subscribe to feature refreshes, so interval and WebSocket
updates can re-render Client Components automatically.
For the shared reliability contract, see Reliability and Error Handling.
Hooks
useFeatureFlag
Check if a single feature is enabled:
'use client'
import { useFeatureFlag } from '@ops-ai/nextjs-toggly-client'
export function NewDashboard() {
const { isEnabled, isLoading, refresh } = useFeatureFlag('new-dashboard')
if (isLoading) {
return <LoadingSpinner />
}
if (!isEnabled) {
return <OldDashboard />
}
return (
<div>
<h1>New Dashboard</h1>
<button onClick={refresh}>Refresh Feature</button>
</div>
)
}
useFeatureOff
Check if a feature is disabled:
'use client'
import { useFeatureOff } from '@ops-ai/nextjs-toggly-client'
export function MaintenanceBanner() {
const { isDisabled, isLoading } = useFeatureOff('maintenance-complete')
if (isLoading) return null
if (isDisabled) {
return <Banner>We're undergoing maintenance</Banner>
}
return null
}
useFeatureGate
Evaluate multiple features together:
'use client'
import { useFeatureGate } from '@ops-ai/nextjs-toggly-client'
export function PremiumFeature() {
// All features must be enabled
const { isEnabled, isLoading } = useFeatureGate(
['premium-access', 'beta-tester'],
'all'
)
if (isLoading) return <LoadingSpinner />
return isEnabled ? <PremiumContent /> : <UpgradePrompt />
}
export function AnyFeatureAccess() {
// Any one feature enables access
const { isEnabled } = useFeatureGate(
['feature-a', 'feature-b', 'feature-c'],
'any'
)
return isEnabled ? <Content /> : <Locked />
}
useFeatures
Get all feature states:
'use client'
import { useFeatures } from '@ops-ai/nextjs-toggly-client'
export function FeatureDebugger() {
const { features, isLoading } = useFeatures()
if (isLoading) return <div>Loading features...</div>
return (
<pre>
{JSON.stringify(features, null, 2)}
</pre>
)
}
useToggly
Access the full Toggly context:
'use client'
import { useToggly } from '@ops-ai/nextjs-toggly-client'
export function UserFeatures() {
const {
features,
isReady,
isLoading,
error,
identity,
setIdentity,
refresh,
isFeatureOn,
evaluateFeatureGate,
} = useToggly()
const handleLogin = async (userId: string) => {
await setIdentity(userId)
}
const checkFeature = async () => {
const isEnabled = await isFeatureOn('my-feature')
console.log('Feature enabled:', isEnabled)
}
return (
<div>
<p>Identity: {identity ?? 'Anonymous'}</p>
<p>Status: {isReady ? 'Ready' : 'Initializing'}</p>
{error && <p>Error: {error.message}</p>}
</div>
)
}
useIdentity
Manage user identity:
setIdentity assigns the new identity and then refreshes remote definitions. It is not transactional: refresh errors are recorded in client state and the previous feature snapshot may be retained; the identity is not rolled back and a resolved promise does not prove a successful definitions fetch. Do not rely on this API to isolate sensitive data between users.
For multi-tenant or entity-gated pages, prefer evaluating on the
server with per-call context / contextKind and
passing booleans into Client Components instead of switching browser identity.
'use client'
import { useIdentity } from '@ops-ai/nextjs-toggly-client'
export function UserProfile() {
const { identity, setIdentity, clearIdentity, isUpdating } = useIdentity()
const handleLogin = async (userId: string) => {
await setIdentity(userId)
}
const handleLogout = async () => {
await clearIdentity()
}
return (
<div>
{identity ? (
<button onClick={handleLogout} disabled={isUpdating}>
Logout ({identity})
</button>
) : (
<button onClick={() => handleLogin('user-123')} disabled={isUpdating}>
Login
</button>
)}
</div>
)
}
Groups, claims, live updates, and signing
Pass groups and claims at provider init for User Claims and group targeting. The client also exposes setContext({ identity, groups, claims }) through useToggly() and useIdentity(). Await the update and inspect errors before relying on refreshed targeting; this is not an authentication API.
<TogglyProvider
config={{
appKey: process.env.NEXT_PUBLIC_TOGGLY_APP_KEY!,
environment: 'Production',
identity: 'user-123',
groups: ['beta', 'enterprise'],
claims: { role: 'admin', plan: 'premium' },
enableLiveUpdates: true, // default — WebSocket sync
verifySignatures: true, // optional ES256 envelope verification
}}
>
{children}
</TogglyProvider>
See Feature filters, Evaluated-signed, and WebSocket sync.
Components
Feature Component
Declarative feature flag rendering:
'use client'
import { Feature } from '@ops-ai/nextjs-toggly-client'
export function Dashboard() {
return (
<>
<Feature
featureKey="analytics-dashboard"
loading={<DashboardSkeleton />}
>
<AnalyticsDashboard />
</Feature>
<Feature featureKey="analytics-dashboard" negate>
<BasicDashboard />
</Feature>
</>
)
}
Negate
Render when a feature is disabled:
'use client'
import { Feature } from '@ops-ai/nextjs-toggly-client'
export function DeprecationNotice() {
return (
<Feature featureKey="new-api" negate>
<Notice>
The legacy API will be deprecated soon. Please migrate to the new API.
</Notice>
</Feature>
)
}
FeatureGate Component
Multiple feature evaluation:
'use client'
import { FeatureGate } from '@ops-ai/nextjs-toggly-client'
export function AdminPanel() {
return (
<>
<FeatureGate
featureKeys={['admin-access', 'beta-tester']}
requirement="all"
>
<AdminContent />
</FeatureGate>
<FeatureGate
featureKeys={['admin-access', 'beta-tester']}
requirement="all"
negate
>
<AccessDenied />
</FeatureGate>
</>
)
}
FeatureSwitch Component
Switch between multiple variants:
'use client'
import { FeatureSwitch } from '@ops-ai/nextjs-toggly-client'
export function Pricing() {
return (
<FeatureSwitch
cases={[
{ featureKey: 'pricing-v3', element: <PricingV3 /> },
{ featureKey: 'pricing-v2', element: <PricingV2 /> },
]}
fallback={<PricingV1 />}
/>
)
}
Configuration Options
<TogglyProvider
config={{
// Required
appKey: string,
// Optional
environment: string, // Default: 'Production'
baseUri: string, // Custom API endpoint
identity: string, // Initial user identity
groups: string[], // Init-time groups for targeting
claims: Record<string, string>, // Init-time claims for targeting
featureDefaults: Record<string, boolean>,
refreshInterval: number, // Auto-refresh interval (ms)
showFeatureDuringEvaluation: boolean, // Show content while checking
enableLiveUpdates: boolean, // WebSocket live updates (default: true)
verifySignatures: boolean, // Verify signed envelopes (default: false)
allowedKeyIds: string[], // Optional JWKS kid allow-list
maxSignatureAgeSeconds: number,
// Client-specific
persistIdentity: boolean, // Save identity to localStorage
identityStorageKey: string, // localStorage key for identity
persistFeatures: boolean, // Cache features in localStorage
featuresStorageKey: string, // localStorage key for features
}}
initialFeatures={Record<string, boolean>} // Server-rendered features
autoInit={boolean} // Auto-initialize on mount (default: true)
>
{children}
</TogglyProvider>
TypeScript
All hooks and components are fully typed:
import type {
UseFeatureFlagReturn,
TogglyContextValue,
FeatureProps,
} from '@ops-ai/nextjs-toggly-client'
// Hook return types
const featureFlag: UseFeatureFlagReturn = useFeatureFlag('my-feature')
// Context type
const context: TogglyContextValue = useToggly()
Entity context
Evaluate flags that depend on an entity (an order, a page, an account) in a Server Component, then pass the result into Client Components.
import { isServerFeatureOn } from '@ops-ai/nextjs-toggly-server'
import { OrderRow } from './order-row'
export default async function OrdersPage({ order }: { order: Order }) {
const express = await isServerFeatureOn('ExpressCheckout', {
context: order,
contextKind: 'Order',
})
return <OrderRow order={order} express={express} />
}
'use client'
export function OrderRow({
order,
express,
}: {
order: Order
express: boolean
}) {
return express ? <ExpressButton order={order} /> : null
}
See also
Best Practices
- Use Server Components First: Prefer Server Components for static feature checks
- Hydrate from Server: Pass server features to avoid loading states
- Handle Loading: Always handle
isLoadingstate for better UX - Persist Identity carefully:
persistIdentitykeeps targeting sticky; identity changes are not transactional with definitions refresh in the core client - Use Components: Prefer declarative components over hooks when possible
- Refresh Strategically: Use
refresh()sparingly to avoid API spam - Multi-tenant / entity gates: Evaluate on the server and pass booleans to the client rather than relying on browser
setIdentity