npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@flagward/react

v0.4.0

Published

React SDK for Flagward - Feature Flags as a Service

Downloads

213

Readme

@flagward/react

React SDK for Flagward - Feature Flags as a Service

Published as flagward-sdk-react up to 0.2.0. Same package, same API — it moved into the @flagward scope so every SDK in this family shares one namespace the project actually owns. Change the name in your package.json and the import; nothing else.

Installation

npm install @flagward/react

Quick Start

import { FlagwardProvider, useFlag } from "@flagward/react";

function App() {
  return (
    <FlagwardProvider apiKey="your-api-key">
      <Dashboard />
    </FlagwardProvider>
  );
}

function Dashboard() {
  const { value: showNewUI } = useFlag("show-new-ui");

  return showNewUI ? <NewDashboard /> : <OldDashboard />;
}

Module format

This package ships as ESM only ("type": "module", with an exports map pointing at dist/index.js). It has no CommonJS build: require("@flagward/react") fails with a clear ERR_PACKAGE_PATH_NOT_EXPORTED/ERR_REQUIRE_ESM error rather than silently loading broken code. Next.js, Vite, and any other bundler-based toolchain resolve ESM packages natively, which covers the realistic ways a React SDK gets consumed. If you have a pure CommonJS build pipeline with no ESM support, import() the package dynamically or open an issue.

How flags are evaluated

This SDK evaluates locally, in the browser. On startup it downloads the full set of flags and their targeting rules for the environment your API key belongs to, then evaluates each flag against the rules on the client. A Server-Sent Events (SSE) stream keeps that local copy fresh as flags change on the server, without a page reload (see "Losing the network" below for what happens when that stream drops).

This has a direct consequence you must know before writing a targeting rule: the rules are visible in the browser. Anything you put in a rule (condition values, user attribute names, percentages, etc.) is downloaded as plain JSON to every client and can be read by opening devtools. Do not encode secrets, internal identifiers you don't want exposed, or anything security-sensitive in a flag's targeting rules — treat them the same way you'd treat any other client-side configuration.

Hooks

Which one

Reach for useFlag. One flag, one decision, one hook — it is what most components need:

const { value, isLoading } = useFlag("new-checkout");

useFlags earns its place in three cases:

  • The keys are not known where you write the code — a debug panel, an admin view, anything that iterates.
  • You need a flag where a hook cannot go — inside an event handler, a callback, a conditional branch. Hooks cannot be conditional; getFlag can.
  • A component reads several flags and one call reads better than five.

useFlag(key, context?)

Evaluates a single flag.

const { value, isLoading, error } = useFlag("new-dashboard");

Returns:

  • value: boolean | undefined — undefined while loading, and for a key this environment does not have
  • isLoading: boolean
  • error: Error | null

The optional second argument adds to the provider's context for this call only. See "Where context comes from" below, which is the part that surprises people.

useFlags()

The whole environment at once.

const { flags, isLoading, error, getFlag } = useFlags();

flags;                              // { "new-checkout": true, ... }
getFlag("show-banner");             // one flag, the provider's context
getFlag("show-banner", { plan: "pro" });   // one flag, plus this context

getFlag is a plain function, so it can be called anywhere — including places a hook cannot go.

useVariant(key, context?)

Resolves the variant of a MULTIVARIATE flag.

const { value, isLoading, error } = useVariant("checkout-flow");

Returns:

  • value: string | undefined — the variant's name, undefined while loading, for a key this environment does not have, and for a flag that is not MULTIVARIATE, is disabled, or is overridden (an override forces a boolean value server-side, so there is no variant)
  • isLoading: boolean
  • error: Error | null

The boolean hooks (useFlag, useFlags) keep returning true once a MULTIVARIATE flag is enabled — they answer "is it on", not "which variant". Use useVariant when the answer needs to be the variant itself.

Where context comes from

Targeting rules are evaluated against a context, and there are two places it can come from. They are not interchangeable:

<FlagwardProvider context={{ plan: "free" }}>   // who the user is
useFlag("beta", { plan: "pro" })                // just this call

A context passed to useFlag belongs to that call. It is not published anywhere: another component cannot see it, and useFlags().flags resolves its map against the provider's context alone. So this is not a contradiction —

const { value } = useFlag("beta", { plan: "pro" });   // true
const { flags } = useFlags();                          // flags.beta === false

— it is two questions with two answers. The call was told "pro"; the map was not. Deliberately: if a context passed in one component reached another component's hook, you would have an invisible channel between parts of an application that share nothing.

So put the user in the provider. Plan, country, id, locale — whatever your rules target — belongs there, where one answer applies everywhere and follows the user through signing in and changing plan. Reach for the per-call context when what you are evaluating is not the current user:

// A list of users. The flag is asked about each row, not about the viewer.
users.map((u) => <Row key={u.id} badge={getFlag("premium-badge", { plan: u.plan })} />)

Putting it together

The user lives in the provider, once:

// providers.tsx
import { FlagwardProvider } from "@flagward/react";

export function Providers({ children }: { children: React.ReactNode }) {
  const { user } = useAuth();

  return (
    <FlagwardProvider
      apiKey={import.meta.env.VITE_FLAGWARD_API_KEY}
      host="https://flags.example.com"
      context={{ plan: user.plan, country: user.country, id: user.id }}
    >
      {children}
    </FlagwardProvider>
  );
}

Signing in or changing plan re-renders this, and every flag in the application follows — you do not tell each component separately.

One decision, one flag:

// Checkout.tsx
function Checkout() {
  const { value: newCheckout, isLoading } = useFlag("new-checkout");

  if (isLoading) return <LegacyCheckout />;

  return newCheckout ? <NewCheckout /> : <LegacyCheckout />;
}

A flag inside a handler, where a hook cannot go:

// CheckoutForm.tsx
function CheckoutForm() {
  const { getFlag } = useFlags();

  const handleSubmit = (data: FormData) => {
    if (getFlag("strict-validation") && !isComplete(data)) {
      return setError("Every field is required.");
    }
    submit(data);
  };

  return <form onSubmit={handleSubmit}>{/* ... */}</form>;
}

And the one case for a per-call context — the flag is about each row, not about whoever is looking:

// UserTable.tsx
function UserTable({ users }: { users: User[] }) {
  const { getFlag } = useFlags();

  return (
    <tbody>
      {users.map((u) => (
        <tr key={u.id}>
          <td>{u.name}</td>
          <td>{getFlag("premium-badge", { plan: u.plan }) ? "★" : null}</td>
        </tr>
      ))}
    </tbody>
  );
}

Multivariate flags

A MULTIVARIATE flag does not just turn a feature on — it hands back which variant a user is in. useFlag and useFlags keep answering "is it on" (true once the flag is enabled, even for a MULTIVARIATE flag); reach for useVariant above when the answer needs to be the variant name.

Put user_id in the provider

Variant assignment is a deterministic bucket of user_id and the flag's key — the same user always lands in the same variant. Without a user_id in context, every user resolves to the flag's control variant. This is the most common surprise: a useVariant call under a provider with no id in context never sees anything but control.

<FlagwardProvider apiKey="your-api-key" context={{ user_id: user.id }}>

A/B/n experiment

function Checkout() {
  const { value: variant, isLoading } = useVariant("checkout-flow");

  if (isLoading) return <LegacyCheckout />;

  switch (variant) {
    case "one-page":
      return <OnePageCheckout />;
    case "express":
      return <ExpressCheckout />;
    default:
      return <LegacyCheckout />;   // control, or undefined
  }
}

Variant as remote configuration

A variant name is also a lookup key, not just a branch to render on:

const COPY: Record<string, string> = {
  control: "Buy now",
  urgent: "Buy now — 3 left",
  social: "Buy now — 200 sold today",
};

function BuyButton() {
  const { value: variant } = useVariant("cta-copy");
  return <button>{COPY[variant ?? ""] ?? "Buy now"}</button>;
}

Segment targeting

A rule can send part of a segment to a specific variant and let the rest fall through to the flag's overall split — only the first matching rule counts:

const { value: variant } = useVariant("pricing-page", { plan: "enterprise" });
// e.g. a rule sends 20% of plan === "enterprise" to "annual-discount";
// the other 80% (and everyone else) falls through to the flag's own split.

Loading

value is undefined while isLoading is true, so branch on loading first when the fallback for "loading" should differ from the fallback for "control":

const { value, isLoading } = useVariant("checkout-flow");

if (isLoading) return <Spinner />;
return value === "express" ? <ExpressCheckout /> : <LegacyCheckout />;

Provider

Wrap your app with FlagwardProvider:

<FlagwardProvider
  apiKey="your-api-key"
  host="https://flags.example.com"  // optional, defaults to https://app.flagward.com
  context={{ userId: "123" }}   // optional, used to evaluate targeting rules
  logLevel="warn"               // optional, see Error reporting below
>
  {children}
</FlagwardProvider>

host defaults to the hosted service at https://app.flagward.com. A self-hosted install passes its own — and that is the case that has to be configured either way, since its operator already knows they are running something.

Next.js

The App Router works with no wrapper of your own. The provider and the hooks declare "use client", so this imports straight into a layout:

// app/layout.tsx
import { FlagwardProvider } from "@flagward/react";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <FlagwardProvider apiKey={process.env.NEXT_PUBLIC_FLAGWARD_API_KEY!}>
          {children}
        </FlagwardProvider>
      </body>
    </html>
  );
}

The package's entry point deliberately does not declare it, so a server component that only wants the evaluator is not dragged over the boundary.

In a server component

Hooks do not run there, but the client does. Reach for @flagward/core — it is already installed as a dependency of this package, and it needs no browser:

// app/page.tsx
import { FlagwardClient } from "@flagward/core";

export default async function Page() {
  const client = new FlagwardClient({ apiKey: process.env.FLAGWARD_API_KEY! });
  await client.init();

  return client.evaluate("new-checkout", { plan: "pro" })
    ? <NewCheckout />
    : <LegacyCheckout />;
}

Note the different key: a server component can read a private environment variable, so the key never reaches the browser. In the provider above it does, which is inherent to evaluating in the client and is why an environment's key grants access to that environment's flags and nothing else.

What is still missing

FlagwardProvider starts its first read in an effect, and effects do not run on the server. So server-rendered HTML always carries isLoading: true and every flag reads as undefined until the browser takes over.

There is no hydration mismatch — the server and the client's first render agree — but there is a flash of whatever your fallback renders, and a flagged component cannot be server-rendered in its real variant. Passing a snapshot from server to client is open work.

Losing the network

Flags are read once and evaluated locally, so a client that loses its connection keeps working: every flag answers from its last known value.

What it cannot do while disconnected is notice a change. When the browser reports the network is back, the SDK re-reads the flags and reopens the update stream if the browser had given up on it, so a change made during the outage is picked up rather than waiting for the next reload.

A stream that is still open is left alone: replacing a working connection would drop events for no reason.

Returning to a backgrounded tab does the same. A machine waking from sleep drops its connection without the browser ever reporting the network as gone, so online never fires and the tab comes back holding whatever it knew before it slept. Both signals share one in-flight refresh, so arriving together does not produce two.

Error reporting

The SDK never throws into your application. A rejected API key, an unreachable server or an unknown flag all resolve to undefined, so your own fallback decides what the user sees and the page keeps rendering.

Because none of that surfaces on its own, the SDK reports it to the console instead:

[Flagward] The API key was rejected. Check that it matches an environment in
your Flagward dashboard.

Each distinct problem is reported once per page load, so a component that re-renders a hundred times does not produce a hundred lines.

logLevel controls how much is reported:

| Value | Reports | |-------|---------| | "warn" (default) | everything: unknown flags, dropped streams, network failures | | "error" | only what cannot work at all: a missing or rejected API key, a missing provider | | "silent" | nothing |

The core

Everything that is not React lives in @flagward/core — the client, the rule evaluator and the reporting — and is re-exported from this package, so installing this one is enough. Reach for the core directly only outside React.

Standalone Client

Use the client directly without React:

import { FlagwardClient } from "@flagward/react";

const client = new FlagwardClient({
  apiKey: "your-api-key",
  host: "https://flags.example.com",   // optional
});

await client.init();

// Every flag's configured on/off state, from the local snapshot.
// Targeting rules are NOT applied here -- there is no user context to
// apply them against.
const flags = client.cachedFlags;

// One flag with targeting rules applied against the context you pass.
// Throws if the key does not exist in this environment.
const showBanner = client.evaluate("show-banner", { plan: "premium" });

// The same, without context and without throwing: an unknown key reads
// as undefined and your own fallback decides what happens.
const maintenance = client.getFlag("maintenance-mode");

// Optional: keep the snapshot fresh via the SSE stream.
client.connect();

// Close it when you are done -- an open stream holds a connection.
client.disconnect();

cachedFlags and evaluate answer different questions. The first says how a flag is configured; the second says what it resolves to for a particular user. Reaching for cachedFlags when you meant evaluate silently ignores every targeting rule on the flag.

License

MIT