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

bash
npm install @sankofa/react

Peer 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.

TSXapp/SankofaSetup.tsx
"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:

TSX
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:

TSX
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:

TSX
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:

TSXapp/checkout/page.tsx
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

SymbolDescription
<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/pulsePulseClient, SurveyRenderer, SurveyState, etc.

What's next

Edit this page on GitHub