# Next.js

## Overview

The Next.js integration provides server-side rendering (SSR) support using a bootstrap pattern. Experiment data is fetched on the server during the initial render and passed to the client, avoiding hydration mismatches.

## Setup the SDK

### Install Packages

```shell
npm install @neonblue-ai/next @neonblue-ai/react-bindings
```

### Initialize the SDK

Wrap your application with `NeonBlueBootstrapProvider` in your root layout. This is an async Server Component that fetches experiment data on the server:

```tsx
// app/layout.tsx
import { NeonBlueBootstrapProvider } from "@neonblue-ai/next";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <NeonBlueBootstrapProvider
          clientKey="client--00000000-0000-0000-0000-000000000000"
          user={{ userId: "00000000" }}
        >
          {children}
        </NeonBlueBootstrapProvider>
      </body>
    </html>
  );
}
```

## Use the SDK

### useExperiment Hook

Use the `useExperiment` hook from `@neonblue-ai/react-bindings` in your Client Components:

```tsx
"use client";

import { useExperiment } from "@neonblue-ai/react-bindings";

export default function MyComponent() {
  const { experiment, isLoading } = useExperiment("my_experiment");

  if (isLoading) {
    return <Loading />;
  }

  return (
    <div>
      <img
        src={experiment?.get(
          "img_url",
          "https://fallback.example.com/default.jpg",
        )}
      />
      <h1>{experiment?.get("h1", "Default Headline")}</h1>
    </div>
  );
}
```

### Async Experiment with Context

For experiments that aren't in the initial cache (cache miss), the SDK fetches from the network. You can pass additional context for these requests:

```tsx
"use client";

import { useExperiment } from "@neonblue-ai/react-bindings";

export default function ProductPage({ productId }: { productId: string }) {
  const { experiment, isLoading } = useExperiment("product_banner", {
    context: {
      productId,
      pageType: "product",
      timestamp: Date.now(),
    },
  });

  if (isLoading) {
    return <Loading />;
  }

  return <img src={experiment?.get("banner_url", "/default-banner.jpg")} />;
}
```

The context is only sent on cache misses when fetching from the network.

### useNeonBlueClient Hook

Access the client directly for more control:

```tsx
"use client";

import { useNeonBlueClient } from "@neonblue-ai/react-bindings";

export default function MyComponent() {
  const { client, getExperimentAsync } = useNeonBlueClient();

  const handleClick = async () => {
    const experiment = await getExperimentAsync("checkout_test", {
      context: { buttonLocation: "header" },
    });
    console.log(experiment.get("variant", "control"));
  };

  return <button onClick={handleClick}>Click me</button>;
}
```

## Configuration

### Provider Props

| Prop            | Type              | Default  | Description                              |
| --------------- | ----------------- | -------- | ---------------------------------------- |
| `clientKey`     | `string`          | required | Your Neon Blue SDK key                   |
| `user`          | `NeonBlueUser`    | required | User object with identifier              |
| `useCookie`     | `boolean`         | `true`   | Enable cookie-based stableId persistence |
| `clientOptions` | `NeonBlueOptions` | `null`   | Additional client configuration          |

### Cookie-Based StableId

By default, `useCookie` is enabled. This ensures consistent experiment assignments across server and client renders by following these steps:

{% stepper %}
{% step %}

### Read stableId from cookies on the server (if present)

The provider will attempt to read an existing stableId from cookies during server rendering.
{% endstep %}

{% step %}

### Generate a new stableId if none exists

If no stableId is present, a new one will be generated to ensure assignment stability.
{% endstep %}

{% step %}

### Persist the stableId to cookies on the client

The generated or existing stableId is saved to cookies on the client to maintain consistency across renders.
{% endstep %}
{% endstepper %}

To disable this behavior:

```tsx
<NeonBlueBootstrapProvider
  clientKey="client--00000000-0000-0000-0000-000000000000"
  user={{ userId: "00000000" }}
  useCookie={false}
>
  {children}
</NeonBlueBootstrapProvider>
```

### Custom API Endpoint

Configure a custom API endpoint via `clientOptions`:

```tsx
<NeonBlueBootstrapProvider
  clientKey="client--00000000-0000-0000-0000-000000000000"
  user={{ userId: "00000000" }}
  clientOptions={{
    networkConfig: {
      api: "https://api.example.com/v1/sdk",
    },
  }}
>
  {children}
</NeonBlueBootstrapProvider>
```

## How It Works

The bootstrap pattern pre-populates the client cache with server-fetched data:

{% stepper %}
{% step %}

### Server Render

`NeonBlueBootstrapProvider` fetches experiment data via `initializeAsync()` during server rendering.
{% endstep %}

{% step %}

### Serialization

Data is serialized to JSON and passed to the client as part of the HTML payload.
{% endstep %}

{% step %}

### Client Hydration

`BootstrapClientSubProvider` initializes the client with the bootstrapped data so it does not need to make an immediate network call.
{% endstep %}

{% step %}

### Hooks Ready

`useExperiment` checks the pre-populated cache first and only fetches from the network on a cache miss.
{% endstep %}
{% endstepper %}

## Changing Users

The `NeonBlueBootstrapProvider` is a Server Component, so user changes require a page refresh to re-run on the server. For client-side login/logout without page refresh, use the `NeonBlueProvider` from `@neonblue-ai/react-bindings` instead:

```tsx
"use client";

import { createContext, useContext, useState } from "react";

import { NeonBlueProvider } from "@neonblue-ai/react-bindings";

type AuthContextType = {
  login: (userId: string) => void;
  logout: () => void;
};

const AuthContext = createContext<AuthContextType | null>(null);

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) throw new Error("useAuth must be used within AuthProvider");
  return context;
}

export default function ClientSideProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  // Start anonymous - empty object uses auto-generated StableId
  const [user, setUser] = useState({});

  const login = (userId: string) => {
    setUser({ userId });
  };

  const logout = () => {
    // Revert to anonymous
    setUser({});
  };

  return (
    <AuthContext.Provider value={{ login, logout }}>
      <NeonBlueProvider
        sdkKey="client--00000000-0000-0000-0000-000000000000"
        user={user}
      >
        {children}
      </NeonBlueProvider>
    </AuthContext.Provider>
  );
}
```

Then use in components:

```tsx
"use client";

import { useAuth } from "./ClientSideProvider";

export function LoginButton() {
  const { login, logout } = useAuth();

  return (
    <>
      <button onClick={() => login("user-123")}>Login</button>
      <button onClick={logout}>Logout</button>
    </>
  );
}
```

The provider automatically re-initializes with the new user and fetches fresh experiment assignments.
