A React provider and hook for Quonfig
npm install @quonfig/react @quonfig/javascript
# or
yarn add @quonfig/react @quonfig/javascript@quonfig/javascript is a peer dependency. npm 7+ installs peers for you, but Yarn does not, so
list it explicitly. TypeScript types are included with the package.
Wrap your component tree in the QuonfigProvider, e.g.
import { QuonfigProvider } from "@quonfig/react";
const WrappedApp = () => {
const context = {
user: { email: "jeffrey@example.com" },
subscription: { plan: "advanced" },
};
const onError = (error) => {
console.error(error);
};
return (
<QuonfigProvider sdkKey={"YOUR_SDK_KEY"} contextAttributes={context} onError={onError}>
<App />
</QuonfigProvider>
);
};Here's an explanation of each provider prop:
| property | required | type | purpose |
|---|---|---|---|
sdkKey |
yes | string |
your Quonfig SDK key |
onError |
no | (error) => void |
callback invoked if quonfig fails to initialize |
contextAttributes |
no | Contexts |
this is the context attributes object you passed when setting up the provider |
timeout |
no | number |
per-request fetch timeout in ms (defaults to ~3s). Keep it above hedgeDelay — see note below. |
hedgeDelay |
no | number |
ms the hedged loader waits for the primary before also firing the secondary in parallel (defaults to ~2s). |
pollInterval |
no | number |
configures quonfig to poll for updates every pollInterval ms. |
initialFlags |
no | Record<string, unknown> |
seed flag values evaluated on the server — see Next.js / RSC integration below. |
timeoutmust stay abovehedgeDelay. The loader fires the primary URL first and, if it is slow (no answer withinhedgeDelay), also fires the secondary in parallel. If you settimeoutat or belowhedgeDelay, the primary is aborted before the hedge can fire, degrading the parallel hedge to error-only failover (the secondary is only tried after the primary fully times out). The SDK logs a warning when this happens. RaisehedgeDelaytoward your primary's measured p99 to contact the secondary less often.
Use the useQuonfig hook to fetch flags and config values:
const Logo = () => {
const { isEnabled } = useQuonfig();
if (isEnabled("new-logo")) {
return <img src={newLogo} className="App-logo" alt="logo" />;
}
return <img src={logo} className="App-logo" alt="logo" />;
};useQuonfig exposes the following:
const { isEnabled, get, loading, contextAttributes } = useQuonfig();Here's an explanation of each property:
| property | example | purpose |
|---|---|---|
isEnabled |
isEnabled("new-logo") |
returns a boolean (default false) if a feature is enabled based on the current context |
get |
get('retry-count') |
returns the value of a flag or config |
loading |
if (loading) { ... } |
a boolean indicating whether quonfig content is being loaded |
contextAttributes |
N/A | this is the context attributes object you passed when setting up the provider |
quonfig |
N/A | the underlying JavaScript quonfig instance |
keys |
N/A | an array of all the flag and config names in the current configuration |
useQuonfig() re-renders every consumer when any flag value changes. For components that only
care about one flag, use useFlag(key) — it subscribes to that single key and skips re-renders when
unrelated flags change.
import { useFlag } from "@quonfig/react";
const Logo = () => {
const showNewLogo = useFlag("new-logo");
return <img src={showNewLogo ? newLogo : logo} alt="logo" />;
};If you have a typed config (via @quonfig/cli generate), useFlag returns the type declared for
the key.
The provider runs on the client (it uses fetch and useEffect), but you often want to seed flag
values on the server so the first paint reflects real data instead of defaults. Pass evaluated flags
through the initialFlags prop on QuonfigProvider.
Fetch flags in a Server Component, then pass the evaluated map to a Client Component that owns the
provider. QuonfigProvider itself must be inside a Client Component because of its hooks.
// app/quonfig-wrapper.tsx — Client Component
"use client";
import { QuonfigProvider } from "@quonfig/react";
export function QuonfigWrapper({ children, initialFlags, contextAttributes }) {
return (
<QuonfigProvider
sdkKey={process.env.NEXT_PUBLIC_QUONFIG_API_KEY!}
contextAttributes={contextAttributes}
initialFlags={initialFlags}
>
{children}
</QuonfigProvider>
);
}// app/layout.tsx — Server Component
import { QuonfigWrapper } from "./quonfig-wrapper";
export default async function RootLayout({ children }) {
// Evaluate flags on the server with @quonfig/node (or similar) and
// hand the flat key/value map to the client provider.
const initialFlags = await evaluateFlagsOnServer({
user: { id: "1" },
});
return (
<html>
<body>
<QuonfigWrapper initialFlags={initialFlags} contextAttributes={{ user: { id: "1" } }}>
{children}
</QuonfigWrapper>
</body>
</html>
);
}initialFlags is the flat-map shape { flagKey: value }, e.g.
{ "new-logo": true, "retry-count": 3 }. When initialFlags is set, the provider hydrates
synchronously on first render — no loading flicker — and skips the initial fetch. Subsequent context
changes still trigger fetches.
export async function getServerSideProps({ req }) {
const initialFlags = await evaluateFlagsOnServer(extractContext(req));
return { props: { initialFlags } };
}
export default function Page({ initialFlags }) {
return (
<QuonfigProvider sdkKey={process.env.NEXT_PUBLIC_QUONFIG_API_KEY} initialFlags={initialFlags}>
<App />
</QuonfigProvider>
);
}- Pass the same
contextAttributeson the server and the client. If the client has different inputs (e.g. it adds attributes fromdocumentorwindow), the first client render will fetch a different evaluation and flag values can flicker. initialFlagsonly applies on the first render. Don't expect changing it later to swap flags out — usesetConfig/poll for that.- Don't combine
initialFlagswithpollIntervalunless you want both — the provider warns at runtime if you do.
QuonfigProvider does render on the server: a "use client" component is still server-rendered for
the first HTML. On the server (typeof window === "undefined"), every provider gets its own
Quonfig() client, seeded only from that render's initialFlags. It never reads or writes the
module singleton there, so one request's flags cannot appear in another request's HTML. The server
render runs no effects, so it does no fetching and no polling.
In the browser, the provider's client identity is keyed by React tree position via
QuonfigClientContext: a top-level provider claims the module singleton (so import { quonfig }
consumers see the same instance), and any nested QuonfigProvider mints a fresh Quonfig() so its
config can't leak into the parent tree. Mount one top-level provider per page: sibling top-level
providers (including ones in separate React roots) share that one singleton, so the last to load
sets the context for all of them and unmounting any of them stops polling for the rest; nest a
provider when you need an independent client.
To build initialFlags on the server, use a new client per request rather than the shared quonfig
singleton. Concurrent requests that init() the singleton can overwrite each other's flags:
import { Quonfig } from "@quonfig/javascript";
const client = new Quonfig();
await client.init({ sdkKey, context, collectEvaluationSummaries: false });
const initialFlags = client.extract();Wrap the component under test in a QuonfigTestProvider and provide a config object to set up your
test state.
e.g. if you wanted to test the following trivial component
function MyComponent() {
const { get, isEnabled, loading } = useQuonfig();
const greeting = get("greeting") || "Greetings";
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1 role="alert">{greeting}</h1>
{isEnabled("secretFeature") && (
<button type="submit" title="secret-feature">
Secret feature
</button>
)}
</div>
);
}You could do the following in jest/rtl
import { QuonfigTestProvider } from '@quonfig/react';
const renderInTestProvider = (config: {[key: string]: any}) => {
render(
<QuonfigTestProvider config={config}>
<MyComponent />
</QuonfigTestProvider>,
);
};
it('shows a custom greeting', async () => {
renderInTestProvider({ greeting: 'Hello' });
const alert = screen.queryByRole('alert');
expect(alert).toHaveTextContent('Hello');
});
it('shows the secret feature when it is enabled', async () => {
renderInTestProvider({ secretFeature: true });
const secretFeature = screen.queryByTitle('secret-feature');
expect(secretFeature).toBeInTheDocument();
});Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. For detailed contributing guidelines, please see CONTRIBUTING.md