Web (multi-package)
@sankofa/react
A minimal React layer for Sankofa — provider, useSankofa hook, and Pulse helpers. For flag/config consumption from React, use the underlying clients directly.
@sankofa/react is a small React companion to the rest of the web SDK. It exposes a <SankofaProvider> context, a useSankofa() hook to access the active client, and Pulse-related helpers. Flag and config consumption from React works through the underlying @sankofa/switch / @sankofa/config packages directly — there are no useFlag / useVariant / useConfig hooks in this package today.
Install
npm install @sankofa/reactPeer dependencies: react ^18 || ^19.
Provider
Wrap your app's root with <SankofaProvider>. The provider gives every child access to the SDK client via useSankofa() and is required for the Pulse helpers.
"use client";
import { Sankofa } from "@sankofa/browser";
import { switchPlugin } from "@sankofa/switch";
import { configPlugin } from "@sankofa/config";
import { pulsePlugin } from "@sankofa/pulse";
import { SankofaProvider } from "@sankofa/react";
if (typeof window !== "undefined") {
Sankofa.init({
apiKey: process.env.NEXT_PUBLIC_SANKOFA_KEY!,
endpoint: "https://api.sankofa.dev",
plugins: [switchPlugin(), configPlugin(), pulsePlugin()],
});
}
export function SankofaSetup({ children }: { children: React.ReactNode }) {
return <SankofaProvider>{children}</SankofaProvider>;
}useSankofa()
Returns the active SDK client. Use it for track, identify, identity reads, and manual flushes:
import { useSankofa } from "@sankofa/react";
function FormFooter() {
const sankofa = useSankofa();
// sankofa.track(...), sankofa.identify(...), sankofa.flush(), etc.
return <button onClick={() => sankofa.track("footer_clicked")}>Click</button>;
}Reading flags + config from React
Use the imperative clients from @sankofa/switch and @sankofa/config. To re-render on change, wrap the read in useState + the client's onChange:
import { useEffect, useState } from "react";
import { getSwitch } from "@sankofa/switch";
import { getConfig } from "@sankofa/config";
function CheckoutButton() {
const flags = getSwitch()!;
const [enabled, setEnabled] = useState(() => flags.getFlag("new_checkout"));
useEffect(() => {
const unsubscribe = flags.onChange("new_checkout", (decision) => {
setEnabled(Boolean(decision.value));
});
return unsubscribe;
}, [flags]);
return enabled ? <NewCheckout /> : <ClassicCheckout />;
}
function MaxUploadInput() {
const config = getConfig()!;
const [maxMB, setMaxMB] = useState(() => config.get<number>("max_upload_mb", 25));
useEffect(() => {
const unsubscribe = config.onChange("max_upload_mb", (decision) => {
setMaxMB(Number(decision.value));
});
return unsubscribe;
}, [config]);
return <input type="file" data-max-size={maxMB * 1024 * 1024} />;
}This pattern is two lines per consumer; if you find yourself duplicating it, factor it into a project-local useFlag / useConfig hook.
Pulse helpers
@sankofa/react re-exports the Pulse types and ships helpers for survey rendering:
import { usePulse, usePulseEvent, SurveyModal } from "@sankofa/react";
function Layout({ children }: { children: React.ReactNode }) {
const pulse = usePulse();
// Listen for survey lifecycle events
usePulseEvent("completed", (event) => {
console.log("Survey", event.surveyId, "completed");
});
return (
<>
{children}
{/* SurveyModal mounts the active survey when one is shown */}
<SurveyModal />
</>
);
}
// Trigger a survey programmatically
function FeedbackButton() {
const pulse = usePulse();
return <button onClick={() => pulse.show("srv_feedback")}>Give feedback</button>;
}usePulse() returns the PulseClient instance from @sankofa/pulse. usePulseEvent(event, listener) subscribes to lifecycle events (shown, completed, dismissed) with proper cleanup on unmount.
Server-side (Next.js App Router, Remix)
The provider is a client component — it must be rendered inside a "use client" boundary. For SSR pages that want flag values rendered on the server, fetch decisions from the engine via REST and pass them down to a hydration-safe wrapper:
async function getInitialDecisions(userId: string) {
const res = await fetch("https://api.sankofa.dev/api/v1/handshake", {
method: "POST",
headers: { "x-api-key": process.env.SANKOFA_KEY!, "content-type": "application/json" },
body: JSON.stringify({ distinct_id: userId, modules: ["switch", "config"] }),
});
return res.json();
}
export default async function Page() {
const initialDecisions = await getInitialDecisions(currentUserId());
// Pass initialDecisions into a client component that seeds the Sankofa client
return <CheckoutClient initial={initialDecisions} />;
}Inside CheckoutClient ("use client"), call Sankofa.init with the initialDecisions opt available on switchPlugin / configPlugin (see those package pages) — this eliminates flicker on first render.
API summary
| Symbol | Description |
|---|---|
<SankofaProvider> | Context provider — wraps your app once at the root. |
useSankofa() | Hook returning the active SDK client. |
usePulse() | Hook returning the Pulse client. |
usePulseEvent(event, listener) | Hook subscribing to Pulse lifecycle events. |
<SurveyModal> | Component that renders the active survey. |
Re-exports from @sankofa/pulse | PulseClient, SurveyRenderer, SurveyState, etc. |