# Next.js

{% hint style="info" %}
You can integrate Hackle with Next.js using the `@hackler/react-sdk` package.

For detailed integration and usage guidance for the Hackle SDK, refer to the [React](/en/development-guide/react.md) guide.
{% endhint %}

## 1. Installation

To use Hackle in Next.js, you need to install `@hackler/react-sdk`.

{% tabs %}
{% tab title="npm" %}

```shell
npm install @hackler/react-sdk
```

{% endtab %}

{% tab title="yarn" %}

```shell
yarn add @hackler/react-sdk
```

{% endtab %}

{% tab title="pnpm" %}

```shell
pnpm add @hackler/react-sdk
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Add the Hackle SDK key to your `.env.development` and `.env.production` files for the respective Development and Production environments.

* You can find your SDK Key in the [SDK Integration Info](https://dashboard.hackle.io/config/sdk-setting) section of the Hackle Dashboard.
  {% endhint %}

```
NEXT_PUBLIC_HACKLE_SDK_KEY=your-hackle-sdk-key
```

## 2. Integration

Hackle supports both [Page Router](https://nextjs.org/docs/pages) and [App Router](https://nextjs.org/docs/app) in Next.js. There are some differences in how to integrate Hackle with each.

#### Creating an Instance

{% tabs %}
{% tab title="App Router" %}

```typescript
// app/hackleClient.client.ts

"use client";

import { createInstance } from "@hackler/react-sdk";

export const hackleClient = createInstance(
  process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
);
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
// app/hackleClient.client.ts

import { createInstance } from "@hackler/react-sdk";

export const hackleClient = createInstance(
  process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
);
```

{% endtab %}
{% endtabs %}

#### App Router - Creating a Provider

```typescript
// app/HackleClientProvider.tsx

"use client";

import { HackleProvider } from "@hackler/react-sdk";
import { hackleClient } from "@/app/hackleClient.client";

export function HackleClientProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <HackleProvider hackleClient={hackleClient} user={{userId: "a-user-id"}} supportSSR>
      {children}
    </HackleProvider>
  );
}
```

#### App Router - Root Layout Setup

```typescript
import { HackleClientProvider } from "@/app/HackleClientProvider";
import "./globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko">
      <body>
        <HackleClientProvider>
          {children}
        </HackleClientProvider>
      </body>
    </html>
  );
}
```

#### Page Router - Using a Provider

```typescript
// pages/_app.tsx

import type { AppProps } from "next/app"
import { HackleProvider } from "@hackler/react-sdk"
import { hackleClient } from "@/app/hackleClient.client"

export default function App({ Component, pageProps }: AppProps) {
  return (
    <HackleProvider hackleClient={hackleClient} user={{ userId: "a-user-id" }} supportSSR>
      <Component {...pageProps} />
    </HackleProvider>
  )
}
```

## 3. Key Features

#### A/B Test Variation Distribution

{% tabs %}
{% tab title="App Router" %}

```typescript
"use client"

import { useLoadableVariationDetail } from "@hackler/react-sdk"
import DecisionComponent from "./DecisionComponent"

interface ClientComponentProps {
  experimentKey: number
}

export default function ClientComponent({ experimentKey }: ClientComponentProps) {
  const { decision, isLoading } = useLoadableVariationDetail(experimentKey)
  if (isLoading) return <div>Loading...</div>
  const { variation, reason, experiment } = decision

  return (
    <div>
      <h3>Client Component</h3>
      <DecisionComponent
        variation={variation}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  )
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { useLoadableVariationDetail } from "@hackler/react-sdk"
import DecisionComponent from "./DecisionComponent"

interface ClientComponentProps {
  experimentKey: number
}

export default function ClientComponent({ experimentKey }: ClientComponentProps) {
  const { decision, isLoading } = useLoadableVariationDetail(experimentKey)
  if (isLoading) return <div>Loading...</div>
  const { variation, reason, experiment } = decision

  return (
    <div>
      <h3>Client Component</h3>
      <DecisionComponent
        variation={variation}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  )
}
```

{% endtab %}
{% endtabs %}

#### Feature Flag Decision

{% tabs %}
{% tab title="App Router" %}

```typescript
"use client"

import { useLoadableFeatureDetail } from "@hackler/react-sdk"
import { FEATURE_FLAG_KEY } from "@/app/constants"
import DecisionComponent from "./DecisionComponent"

export default function ClientComponent() {
  const { decision, isLoading } = useLoadableFeatureDetail(FEATURE_FLAG_KEY)
  if (isLoading) return <div>Loading...</div>
  const { isOn, reason, experiment } = decision

  return (
    <div>
      <h3>Client Component</h3>
      <DecisionComponent
        isOn={isOn}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  )
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { useLoadableFeatureDetail } from "@hackler/react-sdk"
import { FEATURE_FLAG_KEY } from "@/app/constants"
import DecisionComponent from "./DecisionComponent"

export default function ClientComponent() {
  const { decision, isLoading } = useLoadableFeatureDetail(FEATURE_FLAG_KEY)
  if (isLoading) return <div>Loading...</div>
  const { isOn, reason, experiment } = decision

  return (
    <div>
      <h3>Client Component</h3>
      <DecisionComponent
        isOn={isOn}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  )
}
```

{% endtab %}
{% endtabs %}

#### Remote Config

{% tabs %}
{% tab title="App Router" %}

```typescript
"use client";

import useRemoteConfigWithParsing from "@/app/hooks/useRemoteConfigWithParsing";
import { REMOTE_CONFIG_KEY } from "@/app/constants";

const defaultConfig = {
  isDemo: false,
};

export default function ClientComponent() {
  const config = useRemoteConfigWithParsing(REMOTE_CONFIG_KEY, defaultConfig);
  if (config.isLoading) return <div>Loading...</div>

  return (
    <div>
      <h3>Client Component</h3>
      <dl>
        <dt>defaultConfig</dt>
        <dd>{JSON.stringify(defaultConfig, null, 2)}</dd>
        <dt>config</dt>
        <dd>{JSON.stringify(config.value, null, 2)}</dd>
      </dl>
    </div>
  );
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import useRemoteConfigWithParsing from "@/app/hooks/useRemoteConfigWithParsing";
import { REMOTE_CONFIG_KEY } from "@/app/constants";

const defaultConfig = {
  isDemo: false,
};

export default function ClientComponent() {
  const config = useRemoteConfigWithParsing(REMOTE_CONFIG_KEY, defaultConfig);
  if (config.isLoading) return <div>Loading...</div>

  return (
    <div>
      <h3>Client Component</h3>
      <dl>
        <dt>defaultConfig</dt>
        <dd>{JSON.stringify(defaultConfig, null, 2)}</dd>
        <dt>config</dt>
        <dd>{JSON.stringify(config.value, null, 2)}</dd>
      </dl>
    </div>
  );
}
```

{% endtab %}
{% endtabs %}

```typescript
// app/hooks/useRemoteConfigWithParsing.ts

import { useLoadableRemoteConfig } from "@hackler/react-sdk"

export default function useRemoteConfigWithParsing<T>(key: string, defaultValue: T): { value: T; isLoading: boolean } {
  const { remoteConfig, isLoading } = useLoadableRemoteConfig()
  if (isLoading) return { value: defaultValue, isLoading }

  try {
    const configValue = remoteConfig.get(key, JSON.stringify(defaultValue))
    return { value: JSON.parse(configValue), isLoading }
  } catch (error) {
    console.warn("Failed to parse remote config:", error)
    return { value: defaultValue, isLoading }
  }
}
```

#### Event Tracking

```typescript
// app/example.ts

"use client"

import { useTrack } from "@hackler/react-sdk";

export default function Example() {
  const track = useTrack();

  return <button onClick={() => track({ key: "test" })}>test</button>;
```

### Advanced Configuration

Server-side variation distribution in Next.js requires additional setup. When distributing on the server side, you need to carefully manage the user identifier and distinguish between server-side and client-side code.

#### Installation

You need to separately install the SDK for use in the server-side (Node.js) environment.

{% tabs %}
{% tab title="npm" %}

```shell
npm install @hackler/javascript-sdk
```

{% endtab %}

{% tab title="yarn" %}

```shell
yarn add @hackler/javascript-sdk
```

{% endtab %}

{% tab title="pnpm" %}

```shell
pnpm add @hackler/javascript-sdk
```

{% endtab %}
{% endtabs %}

#### Adding the SDK Key

Add the Hackle SDK key to your `.env.development` and `.env.production` files for the respective Development and Production environments.

```
HACKLE_SDK_KEY_SERVER=your-hackle-sdk-key
```

You can find the Server key at <https://dashboard.hackle.io/config/sdk-setting>.

#### Integration

```typescript
// app/hackleClient.server.ts

import { createInstance } from "@hackler/javascript-sdk";

export const hackleClient = createInstance(
  process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
);
```

#### Server-Side Usage

**A/B Test Variation Distribution**

{% tabs %}
{% tab title="App Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";
import DecisionComponent from "./DecisionComponent";

interface ServerComponentProps {
  experimentKey: number;
}

export default async function ServerComponent({
  experimentKey,
}: ServerComponentProps) {
  const userId = "a-user-id";
  await hackleClient.onInitialized();
  const { variation, reason, experiment } = hackleClient.variationDetail(
    experimentKey,
    { userId }
  );

  return (
    <div>
      <h3>Server Component</h3>
      <DecisionComponent
        variation={variation}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  );
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { Decision } from "@hackler/javascript-sdk";
import { hackleClient } from "@/app/HackleClient.server";
import DecisionComponent from "./DecisionComponent";

interface ServerComponentProps {
  decison: Decision;
}

export default async function ServerComponent({
  decison
}: ServerComponentProps) {
  return (
    <div>
      <h3>Server Component</h3>
      <DecisionComponent
        variation={decison.variation}
        reason={String(decison.reason)}
        experimentKey={decison.experiment?.key.toString() ?? "-"}
        experimentVersion={decison.experiment?.version.toString() ?? "-"}
      />
    </div>
  );
}

export const getServerSideProps = async () => {
  const userId = "a-user-id";
  await hackleClient.onInitialized();
  const decision = hackleClient.variationDetail(experimentKey, { userId });
  return {
    props: {
      decision,
    },
  };
};
```

{% endtab %}
{% endtabs %}

**Feature Flag Decision**

{% tabs %}
{% tab title="App Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";
import DecisionComponent from "./DecisionComponent";

interface ServerComponentProps {
  featureFlagKey: number;
}

export default async function ServerComponent({
  featureFlagKey,
}: ServerComponentProps) {
  const userId = "a-user-id";
  await hackleClient.onInitialized()
  const featureFlagDetail = hackleClient.featureFlagDetail(featureFlagKey, {
    userId
  });

  return (
    <div>
      <h3>Server Component</h3>
      <DecisionComponent
        isOn={featureFlagDetail.isOn}
        reason={String(featureFlagDetail.reason)}
        experimentKey={featureFlagDetail.experiment?.key.toString() ?? "-"}
        experimentVersion={
          featureFlagDetail.experiment?.version.toString() ?? "-"
        }
      />
    </div>
  );
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { FeatureFlagDecision } from "@hackler/javascript-sdk";
import { hackleClient } from "@/app/HackleClient.server";
import DecisionComponent from "./DecisionComponent";

interface ServerComponentProps {
  featureFlagDecision: FeatureFlagDecision;
}

export default async function ServerComponent({
  featureFlagDecision,
}: ServerComponentProps) {
  return (
    <div>
      <h3>Server Component</h3>
      <DecisionComponent
        isOn={featureFlagDecision.isOn}
        reason={String(featureFlagDecision.reason)}
        experimentKey={featureFlagDecision.experiment?.key.toString() ?? "-"}
        experimentVersion={
          featureFlagDecision.experiment?.version.toString() ?? "-"
        }
      />
    </div>
  );
}

export const getServerSideProps = async () => {
  const userId = "a-user-id";

  await hackleClient.onInitialized();
  const featureFlagDecision = hackleClient.featureFlagDetail(featureFlagKey, {
    userId,
  });

  return {
    props: {
      featureFlagDecision,
    },
  };
};
```

{% endtab %}
{% endtabs %}

**Remote Config**

{% tabs %}
{% tab title="App Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";

const defaultConfig = {
  isDemo: false,
};

interface ServerComponentProps {
  remoteConfigKey: string;
}

export default async function ServerComponent({
  remoteConfigKey,
}: ServerComponentProps) {
  const userId = "a-user-id";
  await hackleClient.onInitialized()
  const remoteConfig = hackleClient.remoteConfig({
    userId
  });

  const config = JSON.parse(
    remoteConfig.get(remoteConfigKey, JSON.stringify(defaultConfig))
  );

  return (
    <div>
      <h3>Server Component</h3>
      <dl>
        <dt>defaultConfig</dt>
        <dd>{JSON.stringify(defaultConfig, null, 2)}</dd>
        <dt>config</dt>
        <dd>{JSON.stringify(config, null, 2)}</dd>
      </dl>
    </div>
  );
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";

const defaultConfig = {
  isDemo: false,
};

interface ServerComponentProps {
  config: typeof defaultConfig;
}

export default async function ServerComponent({
  config,
}: ServerComponentProps) {
  return (
    <div>
      <h3>Server Component</h3>
      <dl>
        <dt>defaultConfig</dt>
        <dd>{JSON.stringify(defaultConfig, null, 2)}</dd>
        <dt>config</dt>
        <dd>{JSON.stringify(config, null, 2)}</dd>
      </dl>
    </div>
  );
}

export const getServerSideProps = async () => {
  const userId = "a-user-id";
  await hackleClient.onInitialized();
  const remoteConfig = hackleClient.remoteConfig({
    userId,
  });

  const config = JSON.parse(
    remoteConfig.get(remoteConfigKey, JSON.stringify(defaultConfig))
  );

  return {
    props: {
      config: config,
    },
  };
};
```

{% endtab %}
{% endtabs %}

**Event Tracking**

{% tabs %}
{% tab title="App Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";

export default async function Example() {
	hackleClient.track({key: "test"}, {userId: "a-user-id"});
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";

export default async function Example() {
	hackleClient.track({key: "test"}, {userId: "a-user-id"});
}
```

{% endtab %}
{% endtabs %}

#### instrumentation

HackleClient fetches configuration from the Hackle server during initialization and periodically synchronizes afterward. Using `instrumentation.ts` allows you to keep your code simpler without calling `await hackleClient.onInitialized()` on every page. This is recommended for Next.js v15 and above, where `instrumentationHook` is enabled by default.

{% tabs %}
{% tab title="hackleClient.server.ts" %}

```typescript
// app/hackleClient.server.ts

import { createInstance } from "@hackler/javascript-sdk";

declare global {
  var __hackleClient: ReturnType<typeof createInstance> | undefined;
  var __hackleInitialized: boolean | undefined;
}

if (!global.__hackleClient) {
  global.__hackleClient = createInstance(
    process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
  );
}

export async function initializeHackle() {
  if (global.__hackleInitialized) {
    return;
  }

  try {
    await global.__hackleClient!.onInitialized({
      timeout: 10000,
    });

    global.__hackleInitialized = true;
  } catch (error) {
    console.error("❌ Hackle SDK initialization failed:", error);
    throw error;
  }
}

export const hackleClient = global.__hackleClient;
```

{% endtab %}

{% tab title="instrumentation.ts" %}

```typescript
// instrumentation.ts

import { initializeHackle } from "@/app/hackleClient.server";

export async function register() {
  await initializeHackle();
}
```

{% endtab %}
{% endtabs %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.hackle.io/en/development-guide/nextjs-index.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
