# Install StepsKit in a React app

> Add StepsKit to a React app (Vite, Create React App, or plain React). Install the stepskit npm package, render the StepsKit component, and identify users with identify() after login.

*Source: https://stepskit.com/docs/install/react*

For React apps built with Vite, Create React App, or any plain React
setup. Next.js users — there's a [dedicated guide](/docs/install/nextjs.md)
for the App and Pages Routers.

## Install the package

```bash
npm install stepskit
```

Then render `<StepsKit />` once, at the root of your tree in
`src/main.tsx`:

```tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { StepsKit } from "stepskit/react";
import App from "./App";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <StepsKit apiKey="YOUR_API_KEY" />
    <App />
  </StrictMode>,
);
```

The component renders nothing. It loads StepsKit once and is
StrictMode-safe, so React's double-invoked development mount doesn't
inject a second script. Put it above `<App />` so the call queue exists
before any component can call `identify()`.

Once the engine arrives, StepsKit fetches your project's active tours and
waits for instructions.

The queue matters here: the engine loads asynchronously, so an
`identify()` call made during your first render would otherwise arrive
before there's anything to receive it. The package buffers those calls
and replays them in order the moment the engine is ready.

### Identify as you mount

If you already have the user at the root, pass them directly and skip the
separate `identify` call below:

```tsx
<StepsKit
  apiKey="YOUR_API_KEY"
  user={{ id: user.id, email: user.email, plan: user.plan }}
/>
```

The component re-identifies when the *values* change — passing a new
object with the same values does nothing.

### Without the component

Any React setup can call `init()` directly instead — in `src/main.tsx`,
above the render call:

```ts
import stepskit from "stepskit";

stepskit.init("YOUR_API_KEY");
```

Same behaviour; the component just handles mounting for you.

### Alternative: no dependency

If you can't add a dependency, save this as `src/stepskit.ts` and import
it once, **first**, in `src/main.tsx`. There's no IIFE and no wrapper —
an ES module is already a private scope, so nothing leaks onto `window`.

```ts
// Queue API calls made before the SDK finishes loading.
const QUEUED = [
  "identify",
  "setUserAttributes",
  "track",
  "refresh",
  "playTour",
  "stopTour",
  "dismissAnnouncement",
  "on",
  "off",
  "destroy",
  "validateEnvironment",
];

const w = window as unknown as { stepskit?: Record<string, unknown> };
const stepskit = (w.stepskit ??= {});
stepskit._q ??= [];

for (const method of QUEUED) {
  stepskit[method] ??= (...args: unknown[]) =>
    (stepskit._q as unknown[]).push([method, ...args]);
}

// Load the StepsKit SDK asynchronously.
const script = document.createElement("script");
script.async = true;
script.src = "https://cdn.stepskit.com/stepskit.latest.js";
script.dataset.apiKey = "YOUR_API_KEY";
document.head.appendChild(script);

// Marks this file a module, so the consts above stay local to it.
export {};
```

For a plain JavaScript project, name it `src/stepskit.js` and delete the
three type annotations — `as unknown as { stepskit?: Record<string, unknown> }`,
`as unknown[]`, and `: unknown[]`. The rest is valid JavaScript as
written.

The [HTML snippet](/docs/install/js.md) in `index.html` also works — at the
project root for Vite, or `public/index.html` for Create React App. Use
it only if your app already loads its other third-party scripts
(analytics, Sentry) that way; some hosted sandboxes regenerate
`index.html` out from under you.

## Identify the user after login

Anywhere you have the user object — your auth callback, a TanStack Query
`onSuccess`, your Zustand/Redux login action — call `identify`.
StepsKit re-evaluates which tours should play now that it knows who
the visitor is.

```tsx
import stepskit from "stepskit";

async function handleLogin(credentials) {
  const user = await login(credentials);

  stepskit.identify({
    id: user.id,
    email: user.email,
    plan: user.plan,
  });
}
```

## TypeScript

Types ship with the package — there's nothing to declare, and nothing to
configure.

If you installed one of the alternatives above instead, StepsKit only
attaches itself to `window`, so you need the ambient declaration below.
Drop it into `src/`; Vite and Create React App pick up any `.d.ts` there
automatically.

```ts
// src/stepskit.d.ts
// Only needed for the <script> snippet install. If you installed the "stepskit"
// npm package, delete this file — the package ships its own types.
export {};

type StepsKitAttributes = Record<string, string | number | boolean>;

interface StepsKitToursFilteredEntry {
  tourId: string;
  name: string;
  reason:
    | "url_pattern_mismatch"
    | "screen_width_too_narrow"
    | "targeting_failed"
    | "frequency_capped"
    | "inactive";
  detail?: string;
}

interface StepsKitEnvironmentReport {
  apiKey: string; // masked (e.g. "sk_live_a3f...***")
  baseUrl: string;
  visitorId: string | undefined;
  userAttributes: StepsKitAttributes | undefined;
  initialized: boolean;
  isPlaying: boolean;
  currentTourId: string | null;
  toursLoaded: number;
  tooltipsLoaded: number;
  toursFiltered: StepsKitToursFilteredEntry[];
  warnings: string[];
}

declare global {
  interface Window {
    stepskit?: {
      identify(visitor: StepsKitAttributes): Promise<void>;
      setUserAttributes(
        attrs: StepsKitAttributes,
        options?: { autoRefresh?: boolean },
      ): Promise<void>;
      getUserAttributes(): StepsKitAttributes | undefined;
      playTour(tourId: string): Promise<void>;
      stopTour(): void;
      isPlaying(): boolean;
      getTours(): Array<{ id: string; name: string }>;
      refresh(): Promise<void>;
      on(event: string, handler: (...args: unknown[]) => void): void;
      off(event: string, handler: (...args: unknown[]) => void): void;
      dismissAnnouncement(announcementId: string): void;
      destroy(): void;
      validateEnvironment(): StepsKitEnvironmentReport;
      /** No-op today: warns once and discards. Do not wire product events to it. */
      track(event: string, properties?: Record<string, unknown>): void;
    };
  }
}
```

See the [JavaScript API reference](/docs/api.md) for what each method does.

## Without a backend user

For signed-out routes you can simply skip `identify`. Tours targeted at
anonymous visitors will still play, and "show once" rules fall back to
a session-scoped check.
