announcements

Introducing the Zenmanage React SDK

The Zenmanage React SDK adds FlagsProvider, useFlag, and useVariant to your React app, with typed values, safe defaults, and loading and error states you control.

The Zenmanage React SDK is on npm as @zenmanage/react. Wrap your app in FlagsProvider, read flags with useFlag and useVariant, and keep control of what your UI does while a flag loads or when the API can't be reached.

If you've added feature flags to a React app by hand, you know the shape of the code. A client created somewhere at module scope, a useEffect that fetches, a useState to hold the answer, and a flash of the wrong UI while it loads. Every team writes it a little differently, and the loading and failure paths are the ones that get skipped. The React SDK is that wrapper, written once and tested.

What it is

@zenmanage/react is a thin layer over the Zenmanage JavaScript SDK. It doesn't reimplement evaluation, caching, or targeting. It gives you a provider, two hooks, and two components that expose them the way React expects, so a flag behaves the same in your components as it does in the JavaScript quickstart. It needs React 18.2 or newer (React 19 works too) and @zenmanage/sdk 3.5 or newer.

Getting started

Install the package from npm. @zenmanage/sdk and react are peer dependencies, so add the SDK alongside it if your package manager doesn't install peers for you:

npm install @zenmanage/react @zenmanage/sdk

Then wrap your app in FlagsProvider with a client key, and read a flag anywhere below it:

import { FlagsProvider, useFlag } from '@zenmanage/react';

function CheckoutButton() {
  const { value: enabled, isLoading } = useFlag('new-checkout-button', false);

  if (isLoading) {
    return <button disabled>Loading...</button>;
  }

  return <button>{enabled ? 'Checkout (New)' : 'Checkout'}</button>;
}

export function App() {
  return (
    <FlagsProvider environmentToken={import.meta.env.VITE_ZENMANAGE_CLIENT_KEY}>
      <CheckoutButton />
    </FlagsProvider>
  );
}

That's the whole setup. Two things worth noticing: the second argument to useFlag is the value your component gets until the flag resolves, and isLoading is explicit, so you decide whether to render a placeholder, the default, or nothing.

App-wide flag context with FlagsProvider

FlagsProvider initializes the SDK once and shares it through React context, so you aren't creating clients in components or threading one through props. It preloads your flags when it mounts, and it takes an onError callback for preload and refresh failures. If you already have a configured Zenmanage client, pass it as client instead of a token.

The provider is also where targeting context lives. Hand it a Context describing the current user and every flag below it evaluates against that user:

import { Attribute, Context } from '@zenmanage/sdk';
import { FlagsProvider, useFlag } from '@zenmanage/react';

function PremiumBanner() {
  const { value } = useFlag('premium-banner', false);
  return value ? <div>Premium Banner</div> : null;
}

export function App({ user }: { user: { id: string; name: string; plan: string } }) {
  const context = new Context('user', user.name, user.id, [new Attribute('plan', [user.plan])]);

  return (
    <FlagsProvider environmentToken={import.meta.env.VITE_ZENMANAGE_CLIENT_KEY} context={context}>
      <PremiumBanner />
    </FlagsProvider>
  );
}

You don't need to memoize context, defaults, or onError to keep renders cheap. The provider compares them by content, so a new Context object with the same attributes doesn't restart loading. When what's inside the context changes, say a different user signs in, every flag re-evaluates and the hooks report isLoading until the new values land.

Conditional rendering

useFlag is typed by the default you pass. useFlag('k', false).value is a boolean, useFlag('k', 'control').value is a string, and a number works the same way. Pass an object or an array and you read a JSON flag:

const { value: ui } = useFlag('ui-config', { theme: 'light', pageSize: 20 });
const { value: steps } = useFlag<string[]>('onboarding-steps', []);

When you'd rather not branch inside a component, FlagGate does the conditional for you, with fallbacks for the disabled and loading states:

import { FlagGate } from '@zenmanage/react';

<FlagGate flagKey="beta-chat" disabledFallback={<LegacyChat />}>
  <BetaChat />
</FlagGate>

There's also withFlag, a higher-order component that does the same job for code that's organized around wrapping components. Use whichever fits how your codebase already reads.

A/B testing with useVariant

A string flag can carry more than on and off. useVariant reads one as a variant name, falling back to 'control' unless you pass something else, so the baseline experience is what anyone sees if the flag can't be read:

import { useVariant } from '@zenmanage/react';

function CheckoutExperience() {
  const { variant, isLoading } = useVariant('checkout-flow', 'control');

  if (isLoading) {
    return <p>Loading checkout...</p>;
  }

  if (variant === 'one-page') {
    return <OnePageCheckout />;
  }

  return <MultiPageCheckout />;
}

Which users land in which variant is decided in Zenmanage, through targeting rules and percentage rollouts, not in your component. Changing the split doesn't mean shipping a new build.

When the API is unreachable

A flag outage shouldn't take your UI down with it. If the API can't be reached, the underlying SDK falls back to your defaults, so hooks resolve to the default you passed and your components render as usual. Calling refresh() is the exception: it throws, sets error on the provider and every hook, and calls onError, which gives you what you need to show a retry button.

Two things to know before you ship

Browser apps use client keys

Use a client key, prefixed cli_, in the browser. Server keys (srv_) throw there, and mobile keys aren't valid for this package. That's deliberate: a server key in a browser bundle is a leaked secret. Read client keys from your build-time configuration, and keep server keys in server-only environment variables.

Server-side rendering loads flags after hydration

Effects don't run on the server, so a server-rendered page shows each flag's default with isLoading set to true, and the real values arrive once the page hydrates in the browser. If you need real values in the first paint, evaluate the flags on the server with @zenmanage/sdk and pass the results down as props. We'd rather tell you that up front than have you find it in a flash of the wrong UI.

What's next

@zenmanage/react 1.0.0 is on npm now, and the source is on GitHub, including runnable examples and the full changelog. The React quickstart walks through setup end to end, and the SDK overview lists every language we support.

If something in the package gets in your way, tell us at hello@zenmanage.com or open an issue on GitHub.

Enjoyed this article?

Share it with your network.