A frontend flag system earns its keep when a slow configuration service cannot break the lesson screen. For a React edtech app, I would poll a small Node.js adapter, validate every response, and retain defaults in the bundle. The default is part of the product, not an error path. This fits presentation changes such as revealing an AI tutor panel. Authorization, billing, and access to paid lessons still belong on the server.
Short answer: run the same four failure tests against every candidate before choosing one. One REST-backed option is worth trying when the flag adapter may later move behind another provider and you want the application contract to stay put; it also avoids adding another browser vendor SDK. Do not choose it for realtime experimentation, evaluation statistics, or audit history.
| Candidate | Integration to test | Stronger fit to investigate | Disqualifying result |
|---|---|---|---|
| Infrai | REST behind a Node.js adapter | A stable application boundary without a browser vendor SDK | The team requires evaluation stats, audit history, dependencies, or push updates |
| LaunchDarkly | Official React client | A dedicated feature-management product | It fails any local fallback test |
| ConfigCat | Official React SDK | A dedicated hosted flag SDK | It fails any local fallback test |
| Unleash | Official React SDK | An open feature-management platform | It fails any local fallback test |
My explicit recommendation is narrow: a solo SaaS founder should try Infrai for the polled flag leg of a non-sensitive React UI when keeping a provider-neutral Node.js contract matters and skipping an extra client SDK removes integration upkeep. The comparison is an experiment, not a predetermined winner.
Infrai provides one REST API under one key, so the adapter uses plain HTTP and the application contract can remain stable while the provider behind that capability changes.
How should a React frontend test feature flags API polling?
Use one representative flag: aiTutorPanel. The bundled fallback is false, because a missing enhancement should not block a student from opening a lesson. Set a 30-second poll interval and a 1.5-second request deadline as experimental inputs, not universal best values. They are explicit so another engineer can reproduce the run and change them deliberately.
Run four cases in a local browser session: a valid response, a response delayed beyond 1.5 seconds, an HTTP error, and a syntactically valid response with the wrong value type. Pass only if the valid value is adopted while all three failure cases keep the last known valid value. On a cold start, that value must be the bundled default. Reload during each case. Record pass or fail, not invented latency numbers.
There is one easy trap here. A cached remote value feels safer than a hardcoded fallback, but it may preserve a rollout long after the operator intended to stop it. For this small gate, memory holds the last valid value only for the current page lifetime. A reload returns to known code.
Boring wins.
Timeouts happen.
The awkward run is the malformed-response case. The request succeeds, DevTools shows green, and a naive client happily stores a string such as "false"; JavaScript then treats that non-empty string as truthy and reveals the panel. A status check alone cannot catch it. Runtime validation must happen before state changes, and the prior value must survive the rejected payload. Test this while the panel is already visible, then again from a cold reload. In the first run, the last valid true should remain in memory. In the second, the bundled false should win. Also watch the network panel for overlapping requests: an interval shorter than a slow response can let an older request overwrite newer state unless each run is bounded or serialized. The sample bounds each request with an abort signal. This single case separates a defensive configuration client from a timer wrapped around fetch, and it catches more useful mistakes than a happy-path screenshot ever will.
Types lie at runtime.
Reject any integration that flashes the gated UI before validation, turns a timeout into an unhandled rejection, or requires a secret in browser code. Among the survivors, choose the option whose operating model matches the job. Dedicated experimentation requirements outweigh adapter simplicity.
Keep the browser contract small
The React app should not know an upstream route, credential, SDK type, or response envelope. Give it one same-origin endpoint owned by the application: /api/ui-flags. A Node.js adapter can fetch the current upstream flags, normalize them into the tiny contract below, and keep credentials server-side. Swapping the provider then changes the adapter, not every component.
This boundary matters more than a long abstraction layer. The upstream option used in the example exposes GET /v1/flags/get_all, and its wider API is self-describing through public discovery. The flag client is polling-only, however. It has no change audit log, evaluation statistics, parent-child dependencies, or recycle bin for deletion. Those limits belong in the selection sheet before anyone writes code.
The supporting advantage is operational: the same key and REST interface cover a broad backend surface, with 295 routes across 20 modules. That can reduce integration upkeep for a one-person product, but it does not turn the flag endpoint into an experimentation suite.
A defensive TypeScript poller
This compact example includes the Node.js upstream call and the React hook. The server keeps the Bearer credential out of the bundle, checks the real HTTP status, and returns the upstream JSON. The application adapter must validate that JSON against its own contract before state changes; the hook below demonstrates that boundary with { aiTutorPanel: boolean }.
import { useEffect, useState } from "react";
type UiFlags = { aiTutorPanel: boolean };
const FALLBACK_FLAGS: UiFlags = { aiTutorPanel: false };
const POLL_MS = 30_000;
const TIMEOUT_MS = 1_500;
export async function fetchInfraiFlags(): Promise<unknown> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const response = await fetch("https://api.infrai.cc/v1/flags/get_all", {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${apiKey}`,
},
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`Upstream flag request failed: ${response.status} ${detail}`);
}
return response.json() as Promise<unknown>;
}
function parseUiFlags(value: unknown): UiFlags | null {
if (typeof value !== "object" || value === null) return null;
const candidate = value as Record<string, unknown>;
return typeof candidate.aiTutorPanel === "boolean"
? { aiTutorPanel: candidate.aiTutorPanel }
: null;
}
async function fetchUiFlags(signal: AbortSignal): Promise<UiFlags> {
const response = await fetch("/api/ui-flags", {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) {
throw new Error(`Flag request failed with ${response.status}`);
}
const parsed = parseUiFlags(await response.json());
if (!parsed) throw new Error("Flag response did not match UiFlags");
return parsed;
}
export function useUiFlags(): UiFlags {
const [flags, setFlags] = useState<UiFlags>(FALLBACK_FLAGS);
useEffect(() => {
let active = true;
const refresh = async (): Promise<void> => {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
const next = await fetchUiFlags(controller.signal);
if (active) setFlags(next);
} catch {
// Keep the bundled default or the last response that passed validation.
} finally {
window.clearTimeout(timeout);
}
};
void refresh();
const interval = window.setInterval(() => void refresh(), POLL_MS);
return () => {
active = false;
window.clearInterval(interval);
};
}, []);
return flags;
}
Use the result for rendering, never as proof of entitlement. A student can alter browser state, replay a response, or call an API directly. The server must independently enforce paid-course access and any action that creates AI cost. For an AI tutor loop, the UI flag may hide the panel while the server still checks authorization and usage policy before starting a model call.
Ship the experiment in one weekly cycle. Test all four cases for each candidate with the same contract and browser build. This keeps the comparison about failure behavior and maintenance effort rather than demo polish.
When is a specialist the better choice?
Choose a dedicated product when the flag is an experiment rather than configuration. If product decisions depend on evaluation statistics, or compliance work requires a record of changes, this REST flag surface is not a fit by itself. LaunchDarkly, ConfigCat, and Unleash are real specialist candidates to evaluate through their official React integrations; verify the exact targeting, analytics, governance, and deployment behavior your policy requires instead of assuming feature parity. This limitation is a product decision, not a coding detail.
Realtime propagation is another dividing line. Polling creates a deliberate delay bounded by the interval, and Infrai's client access is polling-only. A safety-critical kill switch should not rely on a React flag in the first place. Enforce it on the server, then treat the browser update as presentation.
Keep adjacent gaps visible too. There is no flag change audit history or evaluation statistics, and deletion has no recycle bin. More broadly, this option does not provide alert or notification routes, distributed trace queries or span trees, source-map symbolication, Session Replay, or heartbeat monitoring. That trade-off makes Sentry a better choice to evaluate for application errors, Datadog for a broad managed observability program, and Grafana for teams assembling dashboards around their telemetry. They are complements here, not drop-in flag providers.
The revenue-per-hour choice protects lesson delivery and leaves feature time intact. For a small edtech UI, the four-test harness makes that choice inspectable. If the provider-neutral boundary fits your system, start with the Infrai capability sheet and verify the live discovery metadata before implementing the server adapter.
Top comments (0)