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
166 changes: 125 additions & 41 deletions guides/self-opt-in.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,147 @@
---
description: How to create a beta feature self opt-in page in React with Reflag
description: How to build a beta feature opt-in page with the Reflag React SDK
icon: browser
---

# Beta feature opt-in

Creating a page where users can opt into certain beta/experimental features is straightforward with Reflag and the Reflag React SDK.
Let users opt themselves—or their company—into beta and experimental features with Reflag's React SDK.

The basic concept is to set an attribute on the user/company which denotes that the user has self-opted into a specific feature, for example `optin-<featureKey> = true.` Updating an attribute is very simple from the SDK. Then use that attribute to control who has access to the feature by updating the feature access rules such that users/companies with the attribute `optin-<featureKey>=true` will have access to the feature.
## Quick start

## Step-by-step guide
After enabling end-user opt-in on at least one flag in Reflag, render the available flags and let the current user set their opt-in status.

Here's a step-by-step guide:

1. Select a feature to let people self-opt into.
2. Add the rule: `optin-<featureKey> IS TRUE` to the rules section for all the environments. Replace `<featureKey>` with the actual feature key of the feature.
3. Use the following React component to let users self-opt opt-in to specific features:
This example assumes your app has a `<Suspense>` boundary. See below for an example without `<Suspense>`.

```tsx
import { useUpdateUser, useFlag, ReflagFeatures } from "@reflag/react-sdk";
import { useState } from "react";
import {
type OptInFlag,
useOptInFlags,
useSetOptIn,
} from "@reflag/react-sdk";
import { Spinner } from "your-component-library";

function OptInPage() {
const { flags: optInFlags } = useOptInFlags({ suspense: true });

if (optInFlags.length === 0) {
return <p>No opt-in flags are available.</p>;
}

return optInFlags.map((flag) => (
<OptInFlagCard key={flag.key} flag={flag} />
));
}

function FeatureOptIn({
featureKey,
featureName,
}: {
featureKey: ReflagFeatures;
featureName: string;
}) {
const updateUser = useUpdateUser();
const [sendingUpdate, setSendingUpdate] = useState(false);
const { isEnabled } = useFlag(featureKey);
function OptInFlagCard({ flag }: { flag: OptInFlag }) {
const setOptIn = useSetOptIn();
const [isUpdating, setIsUpdating] = useState(false);
const [updateError, setUpdateError] = useState<string | null>(null);
const label = flag.userOptedIn ? "Cancel opt-in" : `Try ${flag.name}`;

async function updateOptIn() {
setUpdateError(null);
setIsUpdating(true);

try {
const response = await setOptIn(flag.key, {
optedIn: !flag.userOptedIn,
});

if (response?.ok === false) {
throw new Error("Opt-in request failed");
}
} catch {
setUpdateError(`Could not update ${flag.name}. Please try again.`);
} finally {
setIsUpdating(false);
}
}

return (
<div>
<label htmlFor="huddlesOptIn">Opt-in to {featureName} feature</label>
<input
disabled={sendingUpdate}
id="huddlesOptIn"
type="checkbox"
checked={isEnabled}
onChange={() => {
setSendingUpdate(true);
updateUser({
[`optin-${featureKey}`]: isEnabled ? "false" : "true",
}).then(() => {
setSendingUpdate(false);
});
}}
/>
</div>
<section>
<h2>{flag.name}</h2>
{flag.description && <p>{flag.description}</p>}
<button
aria-busy={isUpdating}
disabled={isUpdating}
onClick={updateOptIn}
>
{isUpdating ? (
<Spinner aria-label={`Updating ${flag.name}`} />
) : (
label
)}
</button>
{updateError && <p role="alert">{updateError}</p>}
</section>
);
}
```

### How it works
`setOptIn()` returns a promise that resolves after the SDK applies the latest flag state, confirms the membership change, and notifies components using `useOptInFlags()`. React may not have committed the resulting render yet.

`useOptInFlags()` keeps the list synchronized with Reflag. `useSetOptIn()` changes the current user's opt-in by default and requires the current Reflag context to include a `user.id`.

## Configure a flag for opt-in

1. Open a non-secret flag in Reflag.
2. Go to **Settings > Opt-in**.
3. Enable **End-user opt-in**.
4. Optionally add a **Public description**. The SDK exposes this text so you can display it in your opt-in UI.
5. Save your changes.
6. On the flag's **Access** tab, verify that access is set to **Some** in each environment where users should be able to opt in. Leave the other access rules empty for an opt-in-only feature, or add rules to grant access through either targeting or opt-in.

Secret flags cannot use end-user opt-in because opt-ins are submitted directly from a browser or client using a publishable key.

## Company opt-in

To change the current company's opt-in, pass `scope: "company"`. The current Reflag context must include a `company.id`.

```tsx
setOptIn(flag.key, {
optedIn: !flag.companyOptedIn,
scope: "company",
});
```

User and company opt-ins are independent. Setting `optedIn` to `false` removes only the selected scope, so `isOptedIn` remains `true` while either scope is opted in.

Cancelling every opt-in does not necessarily disable the flag: an access rule may independently enable it for the current context.

## Access behavior

A flag's access setting determines how opt-in membership affects evaluation:

| Access | Behavior |
| --- | --- |
| **No one** | The flag is off for everyone. Existing opt-ins are inactive, and new opt-ins are rejected. |
| **Some** | The flag is enabled when another access rule matches **or** the current user or company opted in. With no other rules, access is opt-in-only. |
| **Everyone** | The flag is enabled for everyone, regardless of opt-in status. |

Disabling end-user opt-in stops new opt-ins and makes existing memberships inactive, but it does not delete them. Re-enabling opt-in reactivates those memberships unless access is set to **No one**.

## Managing loading state with `<ReflagBootstrappedProvider>` and without `<Suspense>`

Only apps using `ReflagBootstrappedProvider` without Suspense need to handle this loading state. Bootstrapped flag data does not include opt-in metadata, so the SDK fetches it when `useOptInFlags()` is first used.

Check the hook's `isLoading` value before rendering an empty state:

```tsx
const { flags: optInFlags, isLoading } = useOptInFlags({ suspense: false });

if (isLoading) {
return <Spinner aria-label="Loading opt-in flags" />;
}

if (optInFlags.length === 0) {
return <p>No opt-in flags are available.</p>;
}
```

The React component above uses [remote attributes](https://reflag.com/changelog/introducing-remote-attributes) to ensure that any feature you've enabled stays enabled between sessions.
With a regular `ReflagProvider`, opt-in metadata arrives as part of the normal flags request, so `useOptInFlags().isLoading` remains `false`. Use `useIsLoading()` or the provider's `loadingComponent` for the normal initial loading state.

### Next steps
## Next steps

* Learn how to manage who has access, and modify the [Targeting rules](../product-handbook/feature-rollouts/feature-targeting-rules.md) in the UI.
Learn how to manage additional access with [Access rules](../product-handbook/feature-rollouts/feature-targeting-rules.md).
63 changes: 0 additions & 63 deletions product-handbook/self-opt-in.md

This file was deleted.