These pages document @evanion/react-feature 0.1.1, the version on npm.
Getting started
By the end of this page a FeatureProvider publishes one store’s decisions, and
a component under it renders the express pickup line for one customer without
taking a prop.
Before you install @evanion/react-feature
To start using @evanion/react-feature, ensure your environment meets these requirements:
- React 18 or 19, the package’s only peer dependency, and
react-dom. - Node 20 or newer and an ESM project, since
@evanion/react-featurehas no CommonJS build. - A store built with
createFeaturesfrom@evanion/feature. Getting started with feature writes a first one. - A targeting key your application already resolves: the signed-in customer’s id, as a string.
Installing @evanion/react-feature
Install @evanion/react-feature and @evanion/feature, the package it reads the
decisions from, with the line for your package manager.
npm install @evanion/react-feature @evanion/featureyarn add @evanion/react-feature @evanion/featurepnpm add @evanion/react-feature @evanion/featureHow to mount FeatureProvider over your store
To provide feature decisions to your component tree, you mount the FeatureProvider. In @evanion/react-feature, the FeatureProvider takes two props to start with: features is the store, and context is the evaluation context every rule in it is read against (including the customer’s targetingKey and now, the instant time windows are read at).
In the example below, Checkout calls useFeatureEnabled and takes no props, and renderToStaticMarkup from react-dom/server stands in for your framework’s server render:
import { } from '@evanion/feature';
import { , } from '@evanion/react-feature';
import { } from 'react-dom/server';
const = ([{ : 'express-pickup', : true }]);
const = {
: 'cust-0042',
: new ('2026-11-01T00:00:00Z'),
};
function () {
if (!('express-pickup')) return <>Standard checkout</>;
return <>Checkout with express pickup</>;
}
const = (
< ={} ={}>
< />
</>,
);
; // -> '<p>Checkout with express pickup</p>'The // -> comment is the HTML the line above it returns. Render the same tree
from your own server, and @evanion/react-feature is working in your project when the
markup matches.
'use client' belongs on the module that mounts the provider in a React Server
Components app such as Next.js, because FeatureProvider holds React context
and useMemo. Other React apps ignore the line.
How to keep the context’s identity stable
Because FeatureProvider memoises resolution on the identity of the context object, a literal written inline is a new object on every render and re-resolves the whole store each pass. To avoid this, hold the context in a useMemo keyed on the values it carries:
// The instant the server rendered at, so both sides read the same clock.
const = new ('2026-11-01T00:00:00Z');
function ({
,
,
}: {
: string;
: ;
}) {
const = (
() => ({ : , }),
[],
);
return (
< ={} ={}>
{}
</>
);
}
const = (
< ="cust-0042">
< />
</>,
);
; // -> '<p>Express</p>'Leaving now out means “the clock at the moment this context was first
resolved”. A tree that has to agree with a server render passes now
explicitly, with the instant the server used.
How to handle keys @evanion/react-feature does not carry
To prevent typos from being indistinguishable from a feature that is off, useFeature, useFeatureEnabled and useVariant throw for a key the provider
does not publish, and the message names the key and the provider. You can use attempt to render an element and return the error message when the render throws:
import { } from '@evanion/feature';
import { , } from '@evanion/react-feature';
import type { } from 'react';
import { } from 'react-dom/server';
const = ([{ : 'express-pickup', : true }]);
function () {
return <>{('express-delivery').}</>;
}
const = (: ) => {
try {
return ();
} catch () {
return ();
}
};
const = (
< ={}>
< />
</>
);
(); // -> 'Error: feature "express-delivery" is not configured in this <FeatureProvider>'
// @ts-expect-error -- the store's keys are checked at compile time
.('express-delivery'); // -> falsefeatures.isEnabled is the other half: the store’s own keys are checked at
compile time, so the misspelling fails before anything runs.
How to bind the hooks to one store’s keys
The basic hooks read a shared context which holds one Decision<FeatureKey> per key, meaning variant is a string and value is unknown. To get type safety, you can use createFeatureContext, which takes a store and returns a provider and four hooks typed by that store’s definitions:
import { } from '@evanion/feature';
import { } from '@evanion/react-feature';
import { } from 'react-dom/server';
const = ([
{
: 'checkout-cta',
: true,
: [
{ : 'control', : 50 },
{ : 'blue', : 50, : { : 'Get it' } },
],
},
]);
const { , } = ();
function () {
const { } = ('checkout-cta');
return < ="button">{?. ?? 'Buy'}</>;
}
function () {
// @ts-expect-error -- 'checkout-button' is not a key of this store
return <>{('checkout-button').}</>;
}
const = (: string) =>
(
< ={{ }}>
< />
</>,
);
('cust-0199'); // -> '<button type="button">Get it</button>'
('cust-0042'); // -> '<button type="button">Buy</button>'useVariant('checkout-cta') is now checked against the keys that store
declares, and its variant is 'control' | 'blue' | undefined. A bound
provider also publishes to the shared context, so @evanion/react-feature’s own
useFeature reads the same decisions underneath it.
The control below mounts that bound provider over the same store. Retype the customer id and the button’s label follows the variant the store assigned:
Retype the id. The same customer always gets the same variant, and about half of them get blue.
<FeatureProvider context={{ targetingKey: 'cust-0199' }}>- variant
- blue
- source
- weighted
- bucket
- 0.805182887474075
- reason
- default-on
The same id always lands in the same variant, from every process and on every
request, and a neighbouring id lands anywhere. source is weighted for an id
the weights bucketed, and bucket is where that id hashed in the half-open
range 0 to 1, which is the number the weights are walked against.
How to hand the server’s decisions to the browser
When a server resolves the flags for a request, it can send those decisions with the page. The FeatureProvider publishes them as they are via the decisions prop. You can generate this set using features.snapshot(context):
const = ([
{
: 'express-pickup',
: true,
: [{ : 'a-share', : { : 25 } }],
},
]);
type = <{ 'express-pickup': never }>;
// app/flags.tsx, a client module ('use client')
function ({
,
,
}: {
: ;
: ;
}) {
return (
< ={} ={}>
{}
</>
);
}
// app/layout.tsx, a Server Component
function ({
,
,
}: {
: string;
: ;
}) {
const = .({ : });
// Props crossing into a client module are serialised; JSON is the same test.
const : = .(.());
return < ={}>{}</>;
}
const = (: string) =>
(
< ={}>
< />
</>,
);
('cust-0042'); // -> '<p>Express</p>'
('cust-0107'); // -> '<p>Standard</p>'The provider resolves nothing beside a supplied set, so every key a component reads has to be in it. The compiler refuses a set whose type lacks a key, and a set that arrived as JSON reaches the provider unchecked:
const = ([
{ : 'express-pickup', : true },
{ : 'gift-cards', : true },
]);
function () {
return <>{('express-pickup') ? 'Express' : 'Standard'}</>;
}
function () {
const { } = ('express-pickup');
const { } = ('express-pickup');
const = .(()).();
return (
<>{`${}/${ ?? 'no variant'}: ${.(', ')}`}</>
);
}
const = .();
.['express-pickup'] = {
: 'express-pickup',
: false,
: 'explicitly-off',
};
// A set shipped as JSON reaches the provider unchecked.
const = .(
.({
...,
: { 'gift-cards': .['gift-cards'] },
}),
);
const = (
< ={} ={}>
< />
</>
);
const = (
< ={} ={}>
< />
</>
);
const = (
< ={} ={}>
< />
</>
);
(); // -> '<p>Standard</p>'
(); // -> 'Error: feature "express-pickup" is not configured in this <FeatureProvider>'
(); // -> '<p>explicitly-off/no variant: express-pickup, gift-cards</p>'Decisions are plain objects and cross the boundary as serialised props. The store does not: it carries functions. So resolution runs on the server, and the browser renders from the set it was handed.
You now have @evanion/react-feature publishing one store’s decisions to a tree and typing the keys a component may name. For a full list of every prop, hook and type, see the API reference. To learn how a share of customers is written down in the first place, see Rollouts.