Framework guides
React SDK
@featurevisor/react gives components hooks that read features and variables, and render again when their values change.
Install#
npm install @featurevisor/react @featurevisor/sdkSet up the provider#
Create a JavaScript SDK instance and pass it to FeaturevisorProvider at the root of your application:
import { StrictMode } from 'react'import { createRoot } from 'react-dom/client'import { createFeaturevisor } from '@featurevisor/sdk'import { FeaturevisorProvider } from '@featurevisor/react'import App from './App'const response = await fetch( 'https://cdn.example.com/featurevisor-all.json',)if (!response.ok) { throw new Error(`Could not fetch datafile: HTTP ${response.status}`)}const f = createFeaturevisor({ datafile: await response.json(), context: { userId: 'user-4', country: 'nl', },})createRoot(document.getElementById('root')).render( <StrictMode> <FeaturevisorProvider instance={f}> <App /> </FeaturevisorProvider> </StrictMode>,)Every hook below reads this instance, with its context. Outside the provider, every hook except useSdk throws an error, and useSdk returns undefined.
Hooks#
useFlag#
useFlag returns whether a flag is enabled:
import { useFlag } from '@featurevisor/react'export function Checkout() { const newCheckout = useFlag('checkout') return newCheckout ? <NewCheckout /> : <OldCheckout />}useFlags#
useFlags returns whether each flag in a list is enabled, in the order of the keys. Name each value after its key, since only its position ties it to the key:
import { useFlags } from '@featurevisor/react'export function Header() { const [checkout, liveChat, wishlist] = useFlags([ 'checkout', 'liveChat', 'wishlist', ]) return ( <nav> {checkout && <CheckoutLink />} {liveChat && <ChatButton />} {wishlist && <WishlistLink />} </nav> )}The list can have any length, and can be built while rendering. The hook follows the keys in the list, not the array itself:
import { useFlags } from '@featurevisor/react'const items = [ { key: 'checkout', label: 'Checkout' }, { key: 'wishlist', label: 'Wishlist' },]export function Menu() { const enabled = useFlags(items.map((item) => item.key)) return ( <ul> {items .filter((item, index) => enabled[index]) .map((item) => ( <li key={item.key}>{item.label}</li> ))} </ul> )}A feature missing from the datafile is false. The returned array stays the same object while its values stay the same, so it can go in an effect's dependencies. In TypeScript, a list written inline gives a tuple of exactly that length, and a list built at runtime gives readonly boolean[]:
const flags = useFlags(['checkout', 'liveChat'])// readonly [boolean, boolean]useEveryFlag#
useEveryFlag returns true when every flag in a list is enabled:
import { useEveryFlag } from '@featurevisor/react'export function AccountSavedItems() { const savedForCheckout = useEveryFlag(['checkout', 'wishlist']) return savedForCheckout ? <SavedItems /> : null}It evaluates in order and stops at the first disabled flag. Flags after that are not evaluated, so a module that records exposure does not see them. Like JavaScript's every, an empty list gives true. In TypeScript, an empty list written inline is an error.
When one feature must never be on without another, require it in the feature's definition instead, as in Managing feature dependencies. Then every application and SDK agrees, not only this component.
useSomeFlag#
useSomeFlag returns true when at least one flag in a list is enabled:
import { useSomeFlag } from '@featurevisor/react'export function AccountHelp() { const showHelp = useSomeFlag(['liveChat', 'wishlist']) return showHelp ? <HelpMenu /> : null}It evaluates in order and stops at the first enabled flag, so a module that records exposure does not see the flags after it. Like JavaScript's some, an empty list gives false. In TypeScript, an empty list written inline is an error.
useVariation#
useVariation returns a feature's variation, or null:
import { useVariation } from '@featurevisor/react'export function Pricing() { const variation = useVariation('pricing') if (variation === 'treatment') { return <NewPricing /> } return <CurrentPricing />}In TypeScript, name the values it can return:
type PricingVariation = 'control' | 'treatment'const variation = useVariation<PricingVariation>('pricing')// PricingVariation | nulluseVariable#
useVariable returns a feature variable from a feature key and a variable key. For user-4, the buy button says Nu kopen:
import { useVariable } from '@featurevisor/react'export function BuyButton() { const label = useVariable('buyButton', 'label') const colour = useVariable('buyButton', 'colour') return <button style={{ background: colour }}>{label}</button>}With one key, it returns a global variable:
const supportEmail = useVariable('supportEmail')In TypeScript, pass the type you expect. It is not checked at runtime. For types that match your project, use code generation:
type CheckoutSettings = { currency: string paymentMethods: string[]}const settings = useVariable<CheckoutSettings>('checkoutSettings')// CheckoutSettings | nulluseRevision#
useRevision returns the revision of the instance's datafile. The component renders again when a datafile with a different revision is set:
import { useRevision } from '@featurevisor/react'export function Footer() { const revision = useRevision() return <small>Datafile revision {revision}</small>}Context and sticky changes do not render it again, and neither does a datafile with the same revision. Before a datafile is set, it returns unknown.
The revision is a string that changes with every new datafile, so it can go in an effect's dependencies to run code after each update:
const revision = useRevision()useEffect(() => { console.info(`Using datafile revision ${revision}`)}, [revision])useFeaturevisor#
useFeaturevisor returns the instance's methods bound to it: isEnabled, getVariation, getVariable and its typed forms, getContext, setContext, setStickyFeatures and setStickyVariables:
import { useEffect } from 'react'import { useFeaturevisor } from '@featurevisor/react'export function UserContext({ user }) { const { setContext } = useFeaturevisor() useEffect(() => { if (user) setContext({ userId: user.id }) }, [user, setContext]) return null}Values read this way do not render the component again when they change. Use them for event handlers and other code outside rendering, and the other hooks for what the component shows. Inside a FeaturevisorScope, its reads include the scope's context.
useSdk#
useSdk returns the instance itself, for anything useFeaturevisor does not cover. What you read through it does not render the component again either.
For a debugging panel, getFeatureEvaluations() on the instance gives a snapshot of several features at once. Call useRevision in the same component to read it again whenever a datafile with a new revision arrives. Context and sticky changes do not render it again.
This component uses it to fetch the datafile again every five minutes, and replace the current one:
import { useEffect } from 'react'import { useSdk } from '@featurevisor/react'const datafileUrl = 'https://cdn.example.com/featurevisor-all.json'const interval = 5 * 60 * 1000export function DatafileRefresh() { const f = useSdk() useEffect(() => { const timer = setInterval(async () => { try { const response = await fetch(datafileUrl) if (response.ok) { f.setDatafile(await response.json(), true) } } catch (error) { console.error('Could not refresh datafile:', error) } }, interval) return () => clearInterval(timer) }, [f]) return null}Each hook renders again where the new datafile affects it, and useRevision shows the new revision. Refresh on an interval has a fuller version that skips a tick while a fetch is still running.
Add context in a component#
Every hook that reads a feature or variable takes context as its last argument, merged over the instance context for that read:
const context = { plan: 'paid',}const colour = useVariable('buyButton', 'colour', context)// '#047857'To change the context for the whole application, such as after sign in, call setContext() on the instance. Every hook that depends on it renders again.
Scopes#
FeaturevisorScope adds context for every hook below it. A part of the page adds what only it knows, such as the product it shows, and its hooks read features with it, without passing context to each call.
Scope part of the page#
A product page knows which product it shows, so it wraps its content in a scope with the product's ID:
import { FeaturevisorScope } from '@featurevisor/react'import { ProductDetails } from '../components/ProductDetails'import { ProductReviews } from '../components/ProductReviews'export function ProductPage({ product }) { return ( <FeaturevisorScope context={{ productId: product.id }}> <ProductDetails product={product} /> <ProductReviews /> </FeaturevisorScope> )}import { useFlag } from '@featurevisor/react'export function ProductReviews() { const productReviews = useFlag('productReviews') return productReviews ? <ReviewList /> : null}Every hook below the scope evaluates with productId, so a feature's conditions can target products as they would any other attribute.
Microfrontends that share one instance through a host application can use a scope the same way, each naming itself in its context, so rules can target where a feature appears.
In the Next.js App Router, the product page can stay a server component. It renders FeaturevisorScope around client components such as ProductReviews, since the scope's context holds plain values. FeaturevisorProvider still goes in a client component, because a server component cannot pass it an instance.
The scope adds to the instance context rather than replacing it, so these hooks still read the user's userId and country. How contexts merge gives the full order.
Inside a scope:
- every hook that reads a feature or variable uses it, and so do the reads of
useFeaturevisor, while itssetContextand sticky setters still change the instance useSdkanduseRevisionare unchanged- a
FeaturevisorProviderwith another instance starts without the scope, since that instance's attributes may differ
A scope that sets userId changes the bucket its hooks use, so set it only when that part of the page is about another user, as in a support tool. A tracking module sees the merged context, so it can tell which product an exposure came from.
Change a scope#
A scope follows its context prop. When the values change, every hook below reads the new context, and its component renders again in the same render as the scope. In this quick view, picking another product changes the scope:
import { useState } from 'react'import { FeaturevisorScope } from '@featurevisor/react'import { ProductPicker } from './ProductPicker'import { ProductReviews } from './ProductReviews'export function QuickView({ products }) { const [product, setProduct] = useState(products[0]) return ( <> <ProductPicker products={products} onPick={setProduct} /> <FeaturevisorScope context={{ productId: product.id }}> <ProductReviews /> </FeaturevisorScope> </> )}Change the context the React way, with state or props, so the scope renders again. A context with the same values, such as the new inline object this component creates on every render, renders nothing below again, and that holds for nested objects, arrays and dates too. Components below that read no feature or variable are not rendered again by the scope.
Nested scopes#
A scope inside another merges over it, so each part adds what it knows to what surrounds it. A category page sets the category once, and each product card adds its own product:
import { FeaturevisorScope, useFlag } from '@featurevisor/react'export function CategoryPage({ category, products }) { return ( <FeaturevisorScope context={{ category: category.key }}> <ul> {products.map((product) => ( <FeaturevisorScope key={product.id} context={{ productId: product.id }} > <ProductCard product={product} /> </FeaturevisorScope> ))} </ul> </FeaturevisorScope> )}function ProductCard({ product }) { const sizeGuide = useFlag('sizeGuide') const productReviews = useFlag('productReviews') return ( <li> {product.name} {sizeGuide && <SizeGuideLink />} {productReviews && <ReviewSummary product={product} />} </li> )}Each card's hooks evaluate with category from the page and their own productId, so sizeGuide can target the category while productReviews targets the product.
An inner scope keeps the keys it sets when the outer scope changes them, and follows the outer scope for the rest. Sibling scopes do not affect each other, and changing an inner scope does not render the components that read only the outer one. A FeaturevisorProvider inside a scope starts again without it, and scopes inside that provider nest from there.
How contexts merge#
Before a hook reads a feature or variable, it merges every context that applies to it into one and evaluates with that. It merges them in this order, each over the ones before it:
- the instance context, set by
createFeaturevisorandsetContext - each enclosing scope, from the outermost to the innermost
- the context passed to the hook, as in Add context in a component
A key that only one of them sets passes through. A key that more than one sets takes its value from the last.
On the category page for shoes, with the instance from Set up the provider, the card for product 1 evaluates sizeGuide and productReviews with the instance's userId and country, the page's category and its own productId:
{ userId: 'user-4', country: 'nl', category: 'shoes', productId: '1',}The order follows where a context sits, not when it changed. A scope keeps the keys it sets even after setContext changes them on the instance, and an inner scope keeps its keys over an outer one. A product page that lists related products can wrap each in a scope of its own: inside it, productId is the related product's, and everywhere else on the page it is the page's.
The merge is shallow, like the instance's own context. A scope that sets an object attribute, such as account, replaces the instance's account as a whole rather than merging the two objects' keys.
The merged context is not stored. The instance keeps its own, and each read merges again. Sticky values sit outside the merge: a feature with a sticky value returns it whatever the merged context holds. To see the context a component evaluates with, call getContext() on what useFeaturevisor returns inside it.
When components render again#
Every hook that reads a feature or variable renders its component again when:
- a new datafile affects its key
- the instance context changes
- sticky values change
- the context of an enclosing
FeaturevisorScopechanges
A datafile update names the features and global variables it affects, including those whose conditions depend on a changed segment, feature or variable, and a hook ignores the rest. A hook that takes a list renders again when the update affects any feature in it. A context or sticky change renders every component that uses a hook, even where the value stays the same.
useRevision renders again only when the revision changes.
Each hook also evaluates on every render. In a long list, read the values once in the parent with useFlags and pass them down, rather than calling useFlag in every row.
Tracking#
To record when a user sees a feature, add a module to the instance and act in its afterEvaluation.
Every hook evaluates each time its component renders, so the module sees the same feature again on every render and in every component that reads it. In development, StrictMode renders each component twice. Record an exposure once per feature and user, not once per evaluation. Google Analytics has a complete example that does this.

