Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/green-flowers-suspend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@reflag/react-sdk": minor
---

Add Suspense support for `useFlag` while flags are loading via provider-level and per-hook `suspense` options.
25 changes: 23 additions & 2 deletions packages/react-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,9 @@ This App Router approach leverages Server Components for server-side flag fetchi

## `<ReflagClientProvider>` component

The `<ReflagClientProvider>` is a lower-level component that accepts a pre-initialized `ReflagClient` instance. This is useful for advanced use cases where you need full control over client initialization or want to share a client instance across multiple parts of your application.
The `<ReflagClientProvider>` is a lower-level component that accepts a `ReflagClient` instance. This is useful for advanced use cases where you need full control over client initialization or want to share a client instance across multiple parts of your application.

In most cases you should initialize the client before rendering this provider. If you pass an idle client and enable `suspense`, a `useFlag()` call that suspends can initialize the client on demand; without Suspense, you must initialize the client yourself.

### Usage

Expand Down Expand Up @@ -509,8 +511,9 @@ function App() {

The `ReflagClientProvider` accepts the following props:

- `client`: A pre-initialized `ReflagClient` instance
- `client`: A `ReflagClient` instance. Prefer passing an already-initialized client; idle clients are initialized on demand only by suspense-enabled `useFlag()` calls.
- `loadingComponent`: Optional React component to show while the client is initializing (same as `ReflagProvider`)
- `suspense`: Optional. Set to `true` to make `useFlag()` suspend while the client is loading

> [!Note]
> Most applications should use `ReflagProvider` or `ReflagBootstrappedProvider` instead of `ReflagClientProvider`. Only use this component when you need the advanced control it provides.
Expand Down Expand Up @@ -549,6 +552,7 @@ The `<ReflagProvider>` initializes the Reflag SDK, fetches flags and starts list
- `staleTimeMs`: Maximum time (in milliseconds) that stale flags will be returned if `staleWhileRevalidate` is true and new flags cannot be fetched.
- `offline`: Provide this option when testing or in local development environments to avoid contacting Reflag servers.
- `loadingComponent` lets you specify an React component to be rendered instead of the children while the Reflag provider is initializing. If you want more control over loading screens, `useFlag()` and `useIsLoading` returns `isLoading` which you can use to customize the loading experience.
- `suspense`: Set to `true` to make `useFlag()` suspend while the provider is loading. Wrap components that call `useFlag()` in React `<Suspense>` boundaries and omit `loadingComponent` if you want Suspense fallbacks to control loading UI.
- `enableTracking`: Set to `false` to stop sending tracking events and user/company updates to Reflag. Useful when you're impersonating a user (defaults to `true`),
- `enableLiveFlagUpdates`: Enables live flag updates over SSE. Defaults to `true` in the React SDK.
- `apiBaseUrl`: Optional base URL for the Reflag API. This also controls the SSE origin used for live flag updates and automated feedback,
Expand Down Expand Up @@ -656,6 +660,23 @@ function StartHuddleButton() {
}
```

#### Suspense loading

Enable `suspense` on the provider to have `useFlag()` throw a promise while `isLoading` is true. The nearest `<Suspense>` boundary will render its fallback until flags are ready.

```tsx
import { Suspense } from "react";
import { ReflagProvider } from "@reflag/react-sdk";

<ReflagProvider publishableKey="..." context={context} suspense>
<Suspense fallback={<Loading />}>
<AppRoutes />
</Suspense>
</ReflagProvider>;
```

You can also opt in for a single call with `useFlag("huddle", { suspense: true })`, or opt out inside a suspense-enabled provider with `{ suspense: false }`.

### `useOptInFlags()` and `useSetOptIn()`

Use these hooks to build an end-user opt-in UI for flags where opt-in is enabled in Reflag.
Expand Down
11 changes: 10 additions & 1 deletion packages/react-sdk/dev/nextjs-bootstrap-demo/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

The purpose of this project is to demonstrate usage integration with the Reflag React SDK.
The purpose of this project is to demonstrate usage integration with the Reflag React SDK using server-side bootstrapping, with `useFlag` wrapped in a React Suspense boundary for any later client-side loading states.

## Getting Started

Configure both SDK keys in `.env.local`:

```bash
REFLAG_SECRET_KEY=sec_...
REFLAG_PUBLISHABLE_KEY=pub_...
```

The example logs a warning and uses offline mode for the affected SDK if either key is missing.

Run the development server:

```bash
Expand Down
21 changes: 21 additions & 0 deletions packages/react-sdk/dev/nextjs-bootstrap-demo/app/client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import { ReflagClient as ReflagNodeClient } from "@reflag/node-sdk";

const secretKey = process.env.REFLAG_SECRET_KEY;
export const publishableKey = process.env.REFLAG_PUBLISHABLE_KEY || "";
export const secretKeyConfigured = Boolean(secretKey);
const offline = process.env.CI === "true" || !secretKey;

declare global {
var serverClient: ReflagNodeClient;
var reflagDemoEnvironmentWarningsShown: boolean | undefined;
}

function warnAboutMissingKeys() {
if (globalThis.reflagDemoEnvironmentWarningsShown) return;

if (!secretKey) {
console.warn(
"[Reflag demo] REFLAG_SECRET_KEY is missing; server-side flag evaluation will run in offline mode.",
);
}
if (!publishableKey) {
console.warn(
"[Reflag demo] REFLAG_PUBLISHABLE_KEY is missing; the browser SDK will run in offline mode.",
);
}
globalThis.reflagDemoEnvironmentWarningsShown = true;
}

/**
Expand All @@ -13,6 +32,8 @@ declare global {
* @returns The server client.
*/
export async function getServerClient() {
warnAboutMissingKeys();

if (!globalThis.serverClient) {
globalThis.serverClient = new ReflagNodeClient({
secretKey,
Expand Down
12 changes: 9 additions & 3 deletions packages/react-sdk/dev/nextjs-bootstrap-demo/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";

import "./globals.css";
import { EnvironmentWarnings } from "@/components/EnvironmentWarnings";
import { ReflagBootstrappedProvider } from "@reflag/react-sdk";

import { getServerClient } from "./client";
import { getServerClient, publishableKey, secretKeyConfigured } from "./client";

const inter = Inter({ subsets: ["latin"] });

Expand All @@ -13,8 +14,6 @@ export const metadata: Metadata = {
description: "Generated by create next app",
};

const publishableKey = process.env.REFLAG_PUBLISHABLE_KEY || "";

export default async function RootLayout({
children,
}: Readonly<{
Expand All @@ -37,9 +36,16 @@ export default async function RootLayout({
return (
<html lang="en">
<body className={inter.className}>
<EnvironmentWarnings
publishableKeyConfigured={Boolean(publishableKey)}
secretKeyConfigured={secretKeyConfigured}
/>
<ReflagBootstrappedProvider
publishableKey={publishableKey}
flags={flags}
feedback={{ enableAutoFeedback: false }}
offline={!publishableKey}
suspense
>
{children}
</ReflagBootstrappedProvider>
Expand Down
16 changes: 15 additions & 1 deletion packages/react-sdk/dev/nextjs-bootstrap-demo/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import Image from "next/image";
import { Suspense } from "react";

import { Flags } from "@/components/Flags";

function FlagsFallback() {
return (
<div className="border border-gray-300 p-6 rounded-xl dark:border-neutral-800 dark:bg-zinc-800/30">
<h3 className="text-xl mb-4">Loading Reflag flags...</h3>
<pre>
<code className="font-mono font-bold">...</code>
</pre>
</div>
);
}

export default async function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
Expand Down Expand Up @@ -37,7 +49,9 @@ export default async function Home() {
/>
</div>

<Flags />
<Suspense fallback={<FlagsFallback />}>
<Flags />
</Suspense>

<div className="mb-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
<a
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import { useEffect } from "react";

type Props = {
publishableKeyConfigured: boolean;
secretKeyConfigured: boolean;
};

export function EnvironmentWarnings({
publishableKeyConfigured,
secretKeyConfigured,
}: Props) {
useEffect(() => {
if (!secretKeyConfigured) {
console.warn(
"[Reflag demo] REFLAG_SECRET_KEY is missing; server-side flag evaluation is running in offline mode.",
);
}
if (!publishableKeyConfigured) {
console.warn(
"[Reflag demo] REFLAG_PUBLISHABLE_KEY is missing; the browser SDK is running in offline mode.",
);
}
}, [publishableKeyConfigured, secretKeyConfigured]);

return null;
}
2 changes: 1 addition & 1 deletion packages/react-sdk/dev/nextjs-flag-demo/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

The purpose of this project is to demonstrate usage integration with the Reflag React SDK.
The purpose of this project is to demonstrate usage integration with the Reflag React SDK, including `useFlag` loading through React Suspense in a server-rendered Next.js App Router page.

## Getting Started

Expand Down
16 changes: 15 additions & 1 deletion packages/react-sdk/dev/nextjs-flag-demo/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import Image from "next/image";
import { Suspense } from "react";

import { Flags } from "@/components/Flags";

function FlagsFallback() {
return (
<div className="border border-gray-300 p-6 rounded-xl dark:border-neutral-800 dark:bg-zinc-800/30">
<h3 className="text-xl mb-4">Loading Reflag flags...</h3>
<pre>
<code className="font-mono font-bold">...</code>
</pre>
</div>
);
}

export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
Expand Down Expand Up @@ -37,7 +49,9 @@ export default function Home() {
/>
</div>

<Flags />
<Suspense fallback={<FlagsFallback />}>
<Flags />
</Suspense>

<div className="mb-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
<a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const Providers = ({ publishableKey, children }: Props) => {
},
}}
fallbackFlags={["fallback-feature"]}
offline={!publishableKey}
suspense
>
{children}
</ReflagProvider>
Expand Down
Loading
Loading