React
If you're using React Native, check out the React Native SDK.
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
⭐ 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
- yarn
npm install @reforge-com/react
yarn add @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.
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
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>
);
};
import { ReforgeProvider } from "@reforge-com/react";
import type { ReactNode } from "react";
const WrappedApp = (): ReactNode => {
const onError = (reason: Error) => {
// error handler for initialization failures
console.error(reason);
};
return (
<ReforgeProvider sdkKey={"REFORGE_FRONTEND_SDK_KEY"} onError={onError}>
<MyApp /> {/* All child components can now use useReforge hook */}
</ReforgeProvider>
);
};
import { ReforgeProvider } from "@reforge-com/react";
const WrappedApp = () => {
const onError = (reason) => {
console.error(reason);
};
return (
<ReforgeProvider sdkKey={"REFORGE_FRONTEND_SDK_KEY"} onError={onError}>
<MyApp />
</ReforgeProvider>
);
};
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.
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
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>
);
};
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.
function MyComponent({someBoolean}: {someBoolean: boolean}) {
// Immediately evaluated regardless of usage
const { welcomeMessage } = useReforge();
return (
<div>
{someBoolean ? (
<p>Welcome!</p>
) : (
<h1>{welcomeMessage}</h1>
)}
</div>
);
};
function MyComponent() {
const reforge = useReforge();
return (
<div>
{someBoolean ? (
<p>Welcome!</p>
) : (
// Only evaluated if used
<h1>{reforge.welcomeMessage}</h1>
)}
</div>
);
};
import { useReforge } from "@reforge-com/react";
import type { ReactElement } from "react";
const Logo = (): ReactElement => {
const { isEnabled } = useReforge(); // get reforge context from provider
if (isEnabled("new-logo")) {
// check if feature flag is enabled
return <img src={newLogo} className="App-logo" alt="logo" />;
}
return <img src={logo} className="App-logo" alt="logo" />; // fallback UI
};
You can also use get
to access flags with other data types.
const { get } = useReforge(); // destructure get function
const stringFlag: string | undefined = get("my-string-flag"); // string config
const numberConfig: number | undefined = get("retry-count"); // number config
const duration = getDuration("api-timeout"); // duration object
import { useReforge } from "@reforge-com/react";
const Logo = () => {
const { isEnabled } = useReforge();
if (isEnabled("new-logo")) {
return <img src={newLogo} className="App-logo" alt="logo" />;
}
return <img src={logo} className="App-logo" alt="logo" />;
};
You can also use get
to access flags with other data types.
const { get } = useReforge();
const flagValue = get("my-string-flag");
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
.
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
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>
);
};
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" }, // user targeting context
subscription: { key: "adv-sub", plan: "advanced" }, // subscription context
};
const onError = (reason: Error) => {
// error handler for failures
console.error(reason);
};
return (
<ReforgeProvider
sdkKey={"REFORGE_FRONTEND_SDK_KEY"} // client SDK key
contextAttributes={contextAttributes} // targeting context for all flags
onError={onError}
>
<App /> {/* All child components inherit this context */}
</ReforgeProvider>
);
};
import { ReforgeProvider } from "@reforge-com/react";
const WrappedApp = () => {
const contextAttributes = {
user: { key: "abcdef", email: "jeffrey@example.com" },
subscription: { key: "adv-sub", plan: "advanced" },
};
const onError = (reason) => {
console.error(reason);
};
return (
<ReforgeProvider
sdkKey={"REFORGE_FRONTEND_SDK_KEY"}
contextAttributes={contextAttributes}
onError={onError}
>
<App />
</ReforgeProvider>
);
};
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
- yarn
npm install mustache
yarn add 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
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
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>
);
};
For non-generated usage, handle templating manually:
import { useReforge } from "@reforge-com/react";
import Mustache from "mustache";
import type { ReactElement } from "react";
const WelcomeMessage = (): ReactElement => {
const { get } = useReforge(); // get reforge hook
// Get the configured object
const welcomeMessageObject = reforge.get("welcome.message");
// Returns: "Hello Alice! Welcome to MyApp. You have 150 credits remaining."
const welcomeText = template
? Mustache.render(welcomeMessageObject.message, {
userName: "Alice",
appName: "MyApp",
creditsCount: 150,
})
: "Welcome!";
// Returns: Buy More Credits
const welcomeCta = welcomeMessageObject.cta || "Buy More";
return (
<div className="welcome-banner">
<h2>{welcomeText}</h2>
<button>{welcomeCta}</button>
</div>
);
};
For raw javascript usage, handle templating manually:
import { useReforge } from "@reforge-com/react";
import Mustache from "mustache";
const WelcomeMessage = () => {
const { get } = useReforge();
// Get the configured object
const welcomeMessageObject = reforge.get("welcome.message");
// Returns: "Hello Alice! Welcome to MyApp. You have 150 credits remaining."
const welcomeText = template
? Mustache.render(welcomeMessageObject.message, {
userName: "Alice",
appName: "MyApp",
creditsCount: 150,
})
: "Welcome!";
// Returns: Buy More Credits
const welcomeCta = welcomeMessageObject.cta || "Buy More";
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.
- TypeScript (Recommended)
- JavaScript
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
);
};
const MyPageComponent = ({ myData, myDataIsLoading }) => {
const { loading: reforgeIsLoading } = useReforge();
if (myDataIsLoading || reforgeIsLoading) {
return <MySpinnerComponent />;
}
return (
// actual page content
);
};
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.
- TypeScript (Recommended)
- JavaScript
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
}
};
const MyComponent = () => {
const { get, loading } = useReforge();
if (loading) {
return <MySpinnerComponent />;
}
switch (get("my-feature-flag")) {
case "new-ui":
return <div>Render the new UI...</div>;
case "old-ui":
default:
return <div>Render the old UI...</div>;
}
};
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.
- TypeScript (Recommended)
- JavaScript
<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>
<ReforgeProvider
sdkKey={"REFORGE_FRONTEND_SDK_KEY"}
contextAttributes={contextAttributes}
onError={onError}
afterEvaluationCallback={(key, value) => {
// call your analytics tool here...in this example we are sending data to posthog
window.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 value | Behavior |
---|---|
PERIODIC_EXAMPLE | Stores context values and context keys. This is the default. |
SHAPE_ONLY | Stores context keys only. |
NONE | Stores 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
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
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();
});
import type { ReactElement } from "react";
function MyComponent(): ReactElement {
const { get, isEnabled, loading } = useReforge();
const greeting = get("greeting") || "Greetings";
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1 role="alert">{greeting}</h1>
{isEnabled("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";
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();
});
function MyComponent() {
const { get, isEnabled, loading } = useReforge();
const greeting = get("greeting") || "Greetings";
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1 role="alert">{greeting}</h1>
{isEnabled("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";
const renderInTestProvider = (config) => {
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.
A fully working example is available as an Example Launch Next.js application.
Overview
The SSR + rehydration pattern works by:
- Server-side: Fetch flag data using a frontend SDK key
- Server-side: Extract the data for client rehydration
- Client-side: Hydrate the React client with pre-fetched data
- Result: No loading states, immediate flag availability
Server-Side Implementation
First, fetch and extract flag data on your server:
- Next.js App Router
- Next.js Pages Router
- Remix
// 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>
);
}
// pages/index.tsx
import { reforge, Contexts } from "@reforge-com/javascript";
import type { GetServerSideProps } from "next";
import { AppWithPreloadedReforge } from "../components/AppWithPreloadedReforge";
interface Props {
initialFlags: Record<string, unknown>;
contextAttributes: any;
}
export default function HomePage({ initialFlags, contextAttributes }: Props) {
return (
<div>
<AppWithPreloadedReforge
initialFlags={initialFlags}
contextAttributes={contextAttributes}
/>
</div>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
// 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.NEXT_PUBLIC_REFORGE_FRONTEND_SDK_KEY!,
context: new Context(contextAttributes),
});
const initialFlags = serverReforge.extract();
return {
props: {
initialFlags,
contextAttributes,
},
};
};
// app/routes/_index.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { reforge } from "@reforge-com/javascript";
import { AppWithPreloadedReforge } from "../components/AppWithPreloadedReforge";
interface LoaderData {
initialFlags: any;
userContext: any;
}
export async function loader({ request }: LoaderFunctionArgs) {
// 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 = serverReforge.extract();
return json<LoaderData>({
initialFlags,
contextAttributes,
});
}
export default function Index() {
const { initialFlags, contextAttributes } = useLoaderData<LoaderData>();
return (
<div>
<AppWithPreloadedReforge
initialFlags={initialFlags}
contextAttributes={contextAttributes}
/>
</div>
);
}
Client-Side Rehydration
Create a client component that hydrates the React provider with server data:
- ⭐ TypeScript + Generated Types (Recommended)
- TypeScript
- JavaScript
// 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>
);
};
// components/AppWithPreloadedReforge.tsx
"use client";
import { ReforgeProvider, useReforge, Contexts } from "@reforge-com/react";
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 { get, isEnabled } = useReforge();
// Flags are immediately available due to hydration
const showNewFeature = isEnabled("new-feature");
const welcomeMessage = get("welcome-message") || "Welcome!";
return (
<div>
<h1>{welcomeMessage}</h1>
{showNewFeature && <NewFeatureComponent />}
<MainContent />
</div>
);
};
// components/AppWithPreloadedReforge.jsx
"use client";
import { ReforgeProvider, useReforge } from "@reforge-com/react";
const AppWithPreloadedReforge = ({
children,
initialFlags,
contextAttributes,
}) => {
return (
<ReforgeProvider
sdkKey={process.env.NEXT_PUBLIC_REFORGE_FRONTEND_SDK_KEY}
contextAttributes={contextAttributes}
initialFlags={initialFlags}
>
<MainApp />
</ReforgeProvider>
);
};
// Your main app component
const MainApp = () => {
const { get, isEnabled } = useReforge();
// Flags are immediately available due to hydration
const showNewFeature = isEnabled("new-feature");
const welcomeMessage = get("welcome-message") || "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
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" } };
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
Framework | Client-side Variable | Server-side Variable |
---|---|---|
Next.js | NEXT_PUBLIC_REFORGE_FRONTEND_SDK_KEY | REFORGE_BACKEND_SDK_KEY |
Remix | REFORGE_FRONTEND_SDK_KEY | REFORGE_BACKEND_SDK_KEY |
Vite | VITE_REFORGE_FRONTEND_SDK_KEY | REFORGE_BACKEND_SDK_KEY |
Create React App | REACT_APP_REFORGE_FRONTEND_SDK_KEY | REFORGE_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
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
property | example | purpose |
---|---|---|
contextAttributes | (see above) | this is the context attributes object you passed when setting up the provider |
getDuration | getDuration("new-logo") | returns a duration type if a flag or config is a duration type |
get | get('retry-count') | returns the value of a flag or config |
isEnabled | isEnabled("new-logo") | returns a boolean (default false ) if a feature is enabled based on the current context |
keys | N/A | an array of all the flag and config names in the current configuration |
loading | if (loading) { ... } | a boolean indicating whether reforge content is being loaded |
reforge | N/A | the underlying JavaScript reforge instance |
While loading
is true, isEnabled
will return false
and getDuration
/get
will return undefined
.