Skip to main content

React

note

If you're using React Native, check out the React Native SDK.

Next.js Environment Variables

When using Next.js, client-side environment variables must be prefixed with NEXT_PUBLIC_. Use NEXT_PUBLIC_REFORGE_FRONTEND_SDK_KEY instead of REFORGE_FRONTEND_SDK_KEY in your .env file:

# .env.local
NEXT_PUBLIC_REFORGE_FRONTEND_SDK_KEY=your-key-here
TypeScript Support

⭐ Recommended: Use the Reforge CLI to generate TypeScript definitions for type-safe access to your flags and configs:

npx @reforge-com/cli generate --targets react-ts

Install the latest version

Use your favorite package manager to install @reforge-com/react npm | github

npm install @reforge-com/react

TypeScript types are included with the package.

Initialize the Client

This client includes a <ReforgeProvider> and useReforge hook.

First, wrap your component tree in the ReforgeProvider, e.g.

First, generate your types:

npx @reforge-com/cli generate --targets react-ts

Then set up your provider (same as TypeScript):

import { ReforgeProvider } from "@reforge-com/react";
import type { ReactNode } from "react";

// The generated types will automatically enhance the provider
const WrappedApp = (): ReactNode => {
const onError = (reason: Error) => {
console.error(reason);
};

return (
<ReforgeProvider sdkKey={"REFORGE_FRONTEND_SDK_KEY"} onError={onError}>
<MyApp />
</ReforgeProvider>
);
};
tip

If you wish for the user's browser to poll for updates to flags, you can pass a pollInterval (in milliseconds) to the ReforgeProvider.

Feature Flags

Now use the useReforge hook to fetch flags. isEnabled is a convenience method for boolean flags.

With generated types, you import the custom typed hook:

// Import the generated typed hook (not the regular useReforge)
import { useReforge } from "./generated/reforge-client";
import type { ReactElement } from "react";

const Logo = (): ReactElement => {
const reforge = useReforge();

// Type-safe camelCase property access with autocomplete
if (reforge.newLogo) {
// boolean type inferred
return <img src={newLogo} className="App-logo" alt="logo" />;
}

return <img src={logo} className="App-logo" alt="logo" />;
};

You get type-safe access to all your flags and configs:

const MyComponent = (): ReactElement => {
const reforge = useReforge();

// All properties are type-safe with IntelliSense
const retryCount = reforge.apiRetryCount; // number type
const welcomeMessage = reforge.welcomeMessage; // string type
const featureEnabled = reforge.coolNewFeature; // boolean type

// Function configs get parameters for templating
const personalizedGreeting = reforge.personalizedWelcome({
name: "John",
}); // Type-safe parameters!

return (
<div>
<h1>{personalizedGreeting}</h1>
{featureEnabled && <NewFeatureComponent />}
<p>Retry attempts: {retryCount}</p>
</div>
);
};
Use Destructuring with Caution

React-specific type safe methods are generated as class getter accessor methods. As a result, if you destructure them directly from the useReforge hook, they are immediately evaluated inline.

DON'T
function MyComponent({someBoolean}: {someBoolean: boolean}) {
// Immediately evaluated regardless of usage
const { welcomeMessage } = useReforge();

return (
<div>
{someBoolean ? (
<p>Welcome!</p>
) : (
<h1>{welcomeMessage}</h1>
)}
</div>
);
};
DO
function MyComponent() {
const reforge = useReforge();

return (
<div>
{someBoolean ? (
<p>Welcome!</p>
) : (
// Only evaluated if used
<h1>{reforge.welcomeMessage}</h1>
)}
</div>
);
};

Using Context

contextAttributes lets you provide context that you can use to segment your users. Usually you will want to define context once when you setup ReforgeProvider.

With generated types, the provider setup is the same, but you get enhanced type safety throughout:

import { ReforgeProvider } from "@reforge-com/react";
import type { Contexts } from "@reforge-com/react";
import type { ReactNode } from "react";

const WrappedApp = (): ReactNode => {
const contextAttributes: Contexts = {
user: { key: "abcdef", email: "jeffrey@example.com" },
subscription: { key: "adv-sub", plan: "advanced" },
};

const onError = (reason: Error) => {
console.error(reason);
};

return (
<ReforgeProvider
sdkKey={"REFORGE_FRONTEND_SDK_KEY"}
contextAttributes={contextAttributes}
onError={onError}
>
<App />
</ReforgeProvider>
);
};

The context then flows through to your components automatically:

// In your components, the typed hook has access to the context
const UserDashboard = (): ReactElement => {
const reforge = useReforge(); // From generated hook

// Context is automatically available - flags are evaluated with your user context
const showPremiumFeatures = reforge.premiumFeatures; // boolean
const userSpecificMessage = reforge.welcomeMessage; // string

return (
<div>
<h1>{userSpecificMessage}</h1>
{showPremiumFeatures && <PremiumDashboard />}
</div>
);
};

Dynamic Config

Config values are accessed the same way as feature flag values using the same camelCase method definitions when using TypeScript. In addition:

  • you can use reforge.get("your.key.here") for all data types.
  • you can use reforge.isEnabled("boolean.key.here") as a convenience for boolean values
  • you can use reforge.getDuration("timeframe.key.here") for time-frame values

By default configs are not sent to frontend SDKs. You must enable access for each individual config. You can do this by checking the "Send to frontend SDKs" checkbox when creating or editing a config.

Mustache Templating

Reforge supports Mustache templating for dynamic string configurations, allowing you to create personalized messages, URLs, and other dynamic content in your React components.

Prerequisites

Install Mustache as a peer dependency:

npm install mustache

Example Configuration

In your Reforge dashboard, create a string configuration with Mustache variables:

  • Configuration Key: welcome.message
  • Configuration Type: json
  • Value:
    {
    "message": "Hello {{userName}}! Welcome to {{appName}}. You have {{creditsCount}} credit(s) remaining.",
    "cta": "Buy More Credits"
    }
  • Zod Schema (optional, for validation):
    z.object({
    message: z.string(),
    cta: z.string(),
    });
  • Send to Client SDKs: ✅ Enabled

Usage Examples

The CLI generates type-safe template functions for React:

// Import the generated typed hook
import { useReforge } from "./generated/reforge-client";
import type { ReactElement } from "react";

const WelcomeMessage = (): ReactElement => {
const reforge = useReforge();

// Get the configured object
const welcomeMessageObject = reforge.welcomeMessage;

// Template functions are generated automatically
// Returns: "Hello Alice! Welcome to MyApp. You have 150 credits remaining."
const welcomeText = welcomeMessageObject.message({
// Type-safe parameters
userName: 'Alice', // string type
appName: 'MyApp', // number type
creditsCount: 150, // string type
});
});

// Returns: Buy More Credits
const welcomeCta = welcomeMessageObject.cta;

return (
<div className="welcome-banner">
<h2>{welcomeText}</h2>
<button>{welcomeCta}</button>
</div>
);
};

Dealing with Loading States

The Reforge client needs to load your feature flags from the Reforge CDN before they are available. This means there will be a brief period when the client is in a loading state. If you call the useReforge hook during loading, you will see the following behavior.

const { get, isEnabled, getDuration, loading } = useReforge();

console.log(loading); // true
console.log(get("my-string-flag)); // undefined for all flags
console.log(getDuration("my-timeframe-flag")); // undefined for all flags
console.log(isEnabled("my-boolean-flag")); // false for all flags

Here are some suggestions for how to handle the loading state.

At the top level of your application or page component

For a single page application, you likely already display a spinner or skeleton component while fetching data from your own backend. In this case, we recommend checking whether Reforge is loaded in the logic for displaying this state. That way you can ensure that Reforge is always loaded before the rest of your component tree renders, and you will not need to check for loading when evaluating individual flags.

import type { ReactElement } from "react";

interface MyPageComponentProps {
myData: any;
myDataIsLoading: boolean;
}

const MyPageComponent = ({ myData, myDataIsLoading }: MyPageComponentProps): ReactElement => {
const { loading: reforgeIsLoading } = useReforge(); // check if flags are loading

if (myDataIsLoading || reforgeIsLoading) { // wait for both data and flags
return <MySpinnerComponent />; // show loading state
}

return (
// actual page content - both data and flags are ready
);
};

However, if you have SEO concerns, such as when using a tool like Docusaurus, you may want to consider one of the following options instead.

In individual components

You can get a loading value back each time you call the useReforge hook and use it to render a spinner or other loading state only for the part of the page that is affected by your flag. This can be a good choice if you are swapping between two different UI treatments and don't want your users to see the page flicker from one to the other after the initial render.

import type { ReactElement } from "react";

const MyComponent = (): ReactElement => {
const { get, loading } = useReforge(); // get flag value and loading state

if (loading) {
// flags not ready yet
return <MySpinnerComponent />; // prevent flickering
}

switch (
get("my-feature-flag") // safe to read flags now
) {
case "new-ui":
return <div>Render the new UI...</div>; // new experience
case "old-ui":
default:
return <div>Render the old UI...</div>; // fallback experience
}
};

Do nothing

If your feature flag is choosing between rendering something and rendering nothing, it may be acceptable to have that content pop-in once Reforge finishes loading. This works because isEnabled will always return false until the Reforge client is loaded.

const MyComponent () => {
const {isEnabled} = useReforge();

return (
<div>
{isEnabled("my-feature-flag") && (
<div>
// Flag content...
</div>
)}
<div>
// Other content...
</div>
</div>
);
}

Tracking Experiment Exposures

If you're using Reforge Launch for A/B testing, you can supply code for tracking experiment exposures to your data warehouse or analytics tool of choice.

<ReforgeProvider
sdkKey={"REFORGE_FRONTEND_SDK_KEY"}
contextAttributes={contextAttributes}
onError={onError}
afterEvaluationCallback={(key: string, value: unknown) => {
// call your analytics tool here...in this example we are sending data to posthog
(window as any).posthog?.capture("Feature Flag Evaluation", {
key,
value,
});
}}
>
<App />
</ReforgeProvider>

afterEvaluationCallback will be called each time you evaluate a feature flag using get or isEnabled.

Telemetry

By default, Reforge will collect summary counts of config and feature flag evaluations to help you understand how your configs and flags are being used in the real world. You can opt out of this behavior by passing collectEvaluationSummaries={false} when initializing ReforgeProvider.

Reforge also stores the context that you pass in. The context keys are used to power autocomplete in the rule editor, and the individual values power the Contexts page for troubleshooting targeting rules and individual flag overrides. If you want to change what Reforge stores, you can pass a different value for collectContextMode.

collectContextMode valueBehavior
PERIODIC_EXAMPLEStores context values and context keys. This is the default.
SHAPE_ONLYStores context keys only.
NONEStores nothing. Context will only be used for rule evaluation.

Testing

Wrap the component under test in a ReforgeTestProvider and provide a config object to set up your test state.

e.g. if you wanted to test the following trivial component

Component using generated types:

// Import the generated typed hook
import { useReforge } from "./generated/reforge-client";
import type { ReactElement } from "react";

function MyComponent(): ReactElement {
const reforge = useReforge();

// Type-safe property access
const greeting = reforge.greeting || "Greetings";

if (reforge.loading) {
return <div>Loading...</div>;
}

return (
<div>
<h1 role="alert">{greeting}</h1>
{reforge.secretFeature && (
<button type="submit" title="secret-feature">
Secret feature
</button>
)}
</div>
);
}

You could do the following in jest/rtl

import { render, screen } from "@testing-library/react";
import { ReforgeTestProvider } from "@reforge-com/react";

// Use the raw config object (camelCase gets converted internally)
const renderInTestProvider = (config: Record<string, any>) => {
render(
<ReforgeTestProvider config={config}>
<MyComponent />
</ReforgeTestProvider>,
);
};

it("shows a custom greeting", async () => {
renderInTestProvider({ greeting: "Hello" });

const alert = screen.queryByRole("alert");
expect(alert).toHaveTextContent("Hello");
});

it("shows the secret feature when it is enabled", async () => {
renderInTestProvider({ secretFeature: true });

const secretFeature = screen.queryByTitle("secret-feature");
expect(secretFeature).toBeInTheDocument();
});

Server-Side Rendering (SSR) to Client-Side Rendering (CSR) Rehydration

For SSR frameworks like Next.js, Remix, or custom React SSR setups, you can eliminate client-side loading states and improve performance by pre-fetching flag data on the server and rehydrating it on the client.

This approach uses the underlying @reforge-com/javascript client's extract and hydrate methods, which are accessible through the React hook.

info

A fully working example is available as an Example Launch Next.js application.

Overview

The SSR + rehydration pattern works by:

  1. Server-side: Fetch flag data using a frontend SDK key
  2. Server-side: Extract the data for client rehydration
  3. Client-side: Hydrate the React client with pre-fetched data
  4. Result: No loading states, immediate flag availability

Server-Side Implementation

First, fetch and extract flag data on your server:

// app/page.tsx or your server component
import { reforge } from "@reforge-com/javascript";
import { AppWithPreloadedReforge } from "../components/AppWithPreloadedReforge";

export default async function Page() {
// Get user context from request, session, etc.
const contextAttributes: Contexts = {
user: { key: "user-123", email: "user@example.com" },
// Add any server-side context you have available
};

// Wait for flags to load
await reforge.init({
sdkKey: process.env.NEXT_PUBLIC_REFORGE_FRONTEND_SDK_KEY!,
context: new Context(contextAttributes),
});

// Extract data for client hydration
const initialFlags = reforge.extract();

return (
<div>
<AppWithPreloadedReforge
initialFlags={initialFlags}
contextAttributes={contextAttributes}
/>
</div>
);
}

Client-Side Rehydration

Create a client component that hydrates the React provider with server data:

// components/AppWithPreloadedReforge.tsx
"use client"; // Next.js App Router client component

import { ReforgeProvider, Contexts } from "@reforge-com/react";
import { useReforge } from "./generated/reforge-client";
import { useEffect, useState } from "react";
import type { ReactElement, ReactNode } from "react";

interface AppWithPreloadedReforgeProps {
children: ReactNode;
initialFlags: Record<string, unknown>;
contextAttributes: Contexts;
}

const AppWithPreloadedReforge = ({
children,
initialFlags,
contextAttributes,
}: AppWithPreloadedReforgeProps): ReactElement => {
return (
<ReforgeProvider
sdkKey={process.env.NEXT_PUBLIC_REFORGE_FRONTEND_SDK_KEY!}
contextAttributes={contextAttributes}
initialFlags={initialFlags}
>
{/* Flags are immediately available, no loading state needed */}
<MainApp />
</ReforgeProvider>
);
};

// Your main app component
const MainApp = (): ReactElement => {
const reforge = useReforge(); // From generated client

// Flags are immediately available due to hydration
const showNewFeature = reforge.newFeature; // No loading check needed!
const welcomeMessage = reforge.welcomeMessage || "Welcome!";

return (
<div>
<h1>{welcomeMessage}</h1>
{showNewFeature && <NewFeatureComponent />}
<MainContent />
</div>
);
};

Alternative: Using the reforge Instance Directly

You can also access the underlying JavaScript client directly from the hook:

import { useReforge } from "@reforge-com/react";
import { useEffect } from "react";

const MyComponent = ({ initialFlags }) => {
const { reforge } = useReforge(); // Access underlying JavaScript client

useEffect(() => {
if (reforge && initialFlags) {
// Hydrate the client with server data
reforge.hydrate(initialFlags);
}
}, [reforge, initialFlags]);

// Rest of your component...
};

Benefits of SSR + Rehydration

  • ⚡ No loading states: Flags are immediately available on first render
  • 🎯 Better SEO: Server-rendered content reflects the actual flag states
  • 🚀 Improved performance: Eliminates client-side API requests for initial flag data
  • 💫 Better UX: No flickering between loading and final states
  • 🎨 Consistent rendering: Server and client render the same content

Important Considerations

Context Consistency

Ensure that the same context attributes are used on both server and client side components. Mismatched context can lead to different flag evaluations and hydration mismatches.

// ❌ Bad: Different context on server vs client
// Server: { user: { key: "123" } }
// Client: { user: { key: "456" } }

// ✅ Good: Same context on both sides
const userContext = { user: { key: "123", email: "user@example.com" } };
SDK Key Requirements

You should only use a REFORGE_FRONTEND_SDK_KEY during this process, for both fetching flags on the server in a react context + the React provider (only receives client-enabled flags).

As a result, only configs with "Send to frontend SDKs" enabled with be available on the client.

Error Handling

Handle cases where server-side flag loading fails:

// Server-side error handling
export const getServerSideProps: GetServerSideProps = async (context) => {
try {
// Get user context from request, session, etc.
const contextAttributes: Contexts = {
user: { key: "user-123", email: "user@example.com" },
// Add any server-side context you have available
};

await reforge.init({
sdkKey: process.env.REFORGE_FRONTEND_SDK_KEY!,
context: new Context(contextAttributes),
});

const initialFlags = reforge.extract();

return { props: { initialFlags, contextAttributes } };
} catch (error) {
console.error("Failed to load flags on server:", error);

// Fallback: let client handle loading normally
return { props: { initialFlags: null, contextAttributes } };
}
};

// Client-side fallback
const AppWithPreloadedReforge = ({ initialFlags, contextAttributes }) => {
return (
<ReforgeProvider
sdkKey={process.env.NEXT_PUBLIC_REFORGE_FRONTEND_SDK_KEY!}
contextAttributes={contextAttributes}
initialFlags={initialFlags}
>
<MainApp />
</ReforgeProvider>
);
};

Framework-Specific Guides

Environment Variables

Many frameworks have specific requirements for client-side environment variables:

Environment Variable Reference

FrameworkClient-side VariableServer-side Variable
Next.jsNEXT_PUBLIC_REFORGE_FRONTEND_SDK_KEYREFORGE_BACKEND_SDK_KEY
RemixREFORGE_FRONTEND_SDK_KEYREFORGE_BACKEND_SDK_KEY
ViteVITE_REFORGE_FRONTEND_SDK_KEYREFORGE_BACKEND_SDK_KEY
Create React AppREACT_APP_REFORGE_FRONTEND_SDK_KEYREFORGE_BACKEND_SDK_KEY

Advanced Patterns

Custom Typed Hooks with createReforgeHook

For advanced users who want to further extend reforge hook functionality, you can use the createReforgeHook factory function:

import { createReforgeHook } from "@reforge-com/react";
import { ReforgeTypesafeReact } from "./generated/reforge-client";

class MyTypesafeReforgeClass extends ReforgeTypesafeReact {
get mySpecialCustomProperty() {
// implementation here
}
}

// Create a custom hook with your typesafe class
const useMyCustomReforge = createReforgeHook(MyTypesafeReforgeClass);

// Use in components
const MyComponent = () => {
const reforge = useMyCustomReforge(); // Fully typed with your generated types

// Access properties with full type safety
const isEnabled = reforge.myFeatureFlag; // boolean
const mySpecialProperty = reforge.mySpecialCustomProperty; // string | number | etc.

return (
<div>
{isEnabled && <FeatureComponent />}
<SpecialPropertyDisplay value={mySpecialProperty} />
</div>
);
};

Use cases:

  • Creating domain-specific hooks (e.g., useFeatureFlags, useAppConfig)
  • Encapsulating complex configuration logic
  • Building custom abstractions over Reforge functionality
Custom Hook Requirements

Please reference the current createReforgeHook implementation for additional details.

You must implement get method + expose the javascript reforge property directly in custom implementations.

Advanced Context Patterns

For complex applications, you can create sophisticated context attribute patterns:

// Dynamic context based on user state
const AppWrapper = ({ user, subscription, device }) => {
const contextAttributes = useMemo(
() => ({
user: {
key: user.id,
email: user.email,
plan: subscription.plan,
signupDate: user.signupDate,
// Add computed attributes
daysSinceSignup: Math.floor(
(Date.now() - new Date(user.signupDate).getTime()) /
(1000 * 60 * 60 * 24),
),
},
subscription: {
key: subscription.id,
plan: subscription.plan,
status: subscription.status,
trialDays: subscription.trialDaysRemaining,
},
device: {
key: device.id,
type: device.type,
mobile: device.mobile,
browserVersion: device.browserVersion,
},
}),
[user, subscription, device],
);

return (
<ReforgeProvider
sdkKey={process.env.REFORGE_FRONTEND_SDK_KEY!}
contextAttributes={contextAttributes}
onError={handleReforgeError}
>
<App />
</ReforgeProvider>
);
};

Reference

useReforge properties

const { isEnabled, get, loading, contextAttributes } = useReforge();

Here's an explanation of each property

propertyexamplepurpose
contextAttributes(see above)this is the context attributes object you passed when setting up the provider
getDurationgetDuration("new-logo")returns a duration type if a flag or config is a duration type
getget('retry-count')returns the value of a flag or config
isEnabledisEnabled("new-logo")returns a boolean (default false) if a feature is enabled based on the current context
keysN/Aan array of all the flag and config names in the current configuration
loadingif (loading) { ... }a boolean indicating whether reforge content is being loaded
reforgeN/Athe underlying JavaScript reforge instance
tip

While loading is true, isEnabled will return false and getDuration/get will return undefined.