# Recipe: interactive client components with the Next.js App Router SDK

> Add interactive Uniform components with the Next.js App Router SDK: a server component with a small client child, a client component in the component map, and the quirks and scores of the visitor.

Source: https://docs.uniform.app/docs/sdk/nextjs-app-router/recipes/client-components

**Goal:** add a component that reacts to the visitor, for example a button that opens a panel. Send as little JavaScript to the browser as possible.

## Prerequisites

- A Next.js app with the SDK. Refer to [Next.js App Router SDK](https://docs.uniform.app/docs/sdk/nextjs-app-router).
- For the rich text example, the `@uniformdev/richtext` package at the same version as the SDK.

## Choose a pattern

| Pattern | Use it when |
| --- | --- |
| [A server component with a client child](#pattern-1-a-server-component-with-a-client-child) (recommended) | Only a small part of the component is interactive. You want visual editing for the text. |
| [A client component in the component map](#pattern-2-a-client-component-in-the-component-map) | The full component is interactive, and it is small. |

## Pattern 1: A server component with a client child

The component in the component map stays a server component. It reads the parameters, and gives plain values to a small client component.

### Step 1: Make the client component

`components/disclosure.tsx`

```tsx
"use client";

import { type ReactNode, useState } from "react";

type DisclosureProps = {
  defaultOpen: boolean;
  summary: ReactNode;
  children: ReactNode;
};

export const Disclosure = ({ defaultOpen, summary, children }: DisclosureProps) => {
  const [open, setOpen] = useState(defaultOpen);

  return (
    <div className="border-b py-2">
      <div className="flex items-center justify-between gap-4">
        <h3 className="font-semibold">{summary}</h3>
        <button type="button" aria-expanded={open} onClick={() => setOpen(!open)}>
          {open ? "Hide" : "Show"}
        </button>
      </div>
      {open ? <div className="pt-2">{children}</div> : null}
    </div>
  );
};
```

The client component knows nothing about Uniform. It gets a boolean and two React nodes.

### Step 2: Make the server component

`components/faq-item.tsx`

```tsx
import {
  type ComponentParameter,
  type ComponentProps,
  UniformRichText,
  UniformText,
} from "@uniformdev/next-app-router/component";
import type { ParameterRichTextValue } from "@uniformdev/richtext";
import { Disclosure } from "./disclosure";

type FaqItemParameters = {
  question?: ComponentParameter<string>;
  answer?: ComponentParameter<ParameterRichTextValue>;
  openByDefault?: ComponentParameter<boolean>;
};

export const FaqItem = ({
  parameters: { question, answer, openByDefault },
  component,
  context,
}: ComponentProps<FaqItemParameters>) => (
  <Disclosure
    // Plain values only: a boolean and React elements.
    defaultOpen={openByDefault?.value === true || context.isContextualEditing}
    summary={
      question ? <UniformText component={component} parameter={question} placeholder="Enter a question" /> : null
    }
  >
    {answer ? <UniformRichText component={component} parameter={answer} placeholder="Enter an answer" /> : null}
  </Disclosure>
);
```

Register `FaqItem` in `components/resolveComponent.tsx`.

`FaqItem` makes the `UniformText` and `UniformRichText` elements on the server. It gives them to `Disclosure` as props. Thus authors can still edit the text in Canvas. In Canvas, `context.isContextualEditing` is `true`, so the panel is open and the author can see the answer.

## Pattern 2: A client component in the component map

You can also put `"use client"` in the file of a registered component:

`components/promo-code.tsx`

```tsx
"use client";

import type { ComponentParameter, ComponentProps } from "@uniformdev/next-app-router/component";
import { useState } from "react";

type PromoCodeParameters = {
  label?: ComponentParameter<string>;
  code?: ComponentParameter<string>;
};

export const PromoCode = ({ parameters: { label, code } }: ComponentProps<PromoCodeParameters>) => {
  const [visible, setVisible] = useState(false);

  if (!code?.value) return null;

  return (
    <div className="rounded border p-4">
      <p>{label?.value ?? "Your discount code"}</p>
      {visible ? (
        <code className="text-xl">{code.value}</code>
      ) : (
        <button type="button" onClick={() => setVisible(true)}>
          Show the code
        </button>
      )}
    </div>
  );
};
```

Register it in the component map as usual. `components/resolveComponent.tsx` stays a server file:

`components/resolveComponent.tsx`

```tsx
import type { ResolveComponentFunction, ResolveComponentResult } from "@uniformdev/next-app-router";
import type { ComponentProps } from "@uniformdev/next-app-router/component";

import { FaqItem } from "./faq-item";
import { InterestCard } from "./interest-card";
import { PromoCode } from "./promo-code";

const componentMap: Record<string, ResolveComponentResult["component"]> = {
  faqItem: FaqItem,
  interestCard: InterestCard,
  promoCode: PromoCode,
};

const NotFound = ({ type }: ComponentProps) => <div>Component not found: {type}</div>;

export const resolveComponent: ResolveComponentFunction = ({ component }) => ({
  component: componentMap[component.type] ?? NotFound,
});
```

### The serialization rule

The SDK renders each component on the server. For a client component, React must serialize the props and send them to the browser. Props of a client component must be serializable. For example, plain objects, arrays, strings, numbers, booleans, `null` and React elements are serializable. Functions are not serializable.

`ComponentProps` obeys this rule:

| Prop | Contents |
| --- | --- |
| `type`, `variant` | Strings |
| `parameters` | The parameter data from Uniform, plus `parameterId` |
| `slots` | For each slot: its name, and for each item its `_id`, its variant data and the rendered child as a React element |
| `component` | IDs, the slot name and the slot index |
| `context` | The composition ID and type, the state, `isContextualEditing`, `matchedRoute`, `dynamicInputs` and `pageState` |

The SDK also renders its own client components with these props, for example the component for personalizations.

> **Warning:**
>
> The browser gets all props of a client component, also the parameters that it does not use. Do not keep secret values in parameters of a client component. A server component with a client child (pattern 1) sends only the values that you give to the child.

## Read the quirks and scores of the visitor

`useQuirks` and `useScores` give the data of the visitor in a client component. The component renders again when the data changes:

`components/visitor-greeting.tsx`

```tsx
"use client";

import { useQuirks, useScores } from "@uniformdev/next-app-router/component";

export const VisitorGreeting = () => {
  const quirks = useQuirks();
  const scores = useScores();

  const country = quirks["vc-country"];
  const isDeveloper = (scores?.developer ?? 0) > 50;

  return (
    <p>
      {country ? `Welcome, visitor from ${country}.` : "Welcome."}
      {isDeveloper ? " Read our API docs." : null}
    </p>
  );
};
```

On the server, `useQuirks` returns an empty object, and `useScores` returns `undefined`. Thus the server HTML always shows "Welcome.". When the browser Context is ready, the component renders again with the data of the visitor. Show a neutral default in the server HTML.

The `vc-country` quirk comes from the Vercel geolocation headers. Refer to [Personalization and A/B tests](https://docs.uniform.app/docs/sdk/nextjs-app-router/personalization#quirks-from-vercel-geolocation).

> **Tip:**
>
> To show different content to different visitors, use a personalization in Uniform. The SDK renders the variants for you. Use the hooks only for small changes that do not need a personalization.

## Update the Context

`useUniformContext` gives the browser Context. Call `context.update` to change the data of the visitor. Personalizations on the page then select their variants again.

### Set a quirk

`components/member-button.tsx`

```tsx
"use client";

import { useUniformContext } from "@uniformdev/next-app-router/component";

export const MemberButton = () => {
  const { context } = useUniformContext();

  return (
    <button type="button" disabled={!context} onClick={() => context?.update({ quirks: { member: "yes" } })}>
      I am a member
    </button>
  );
};
```

Make the quirk `member` in Uniform, so that personalization criteria can use it.

### Add an enrichment score

The server component reads the enrichment from its parameters. It gives the values as strings to the client button:

`components/interest-button.tsx`

```tsx
"use client";

import { useUniformContext } from "@uniformdev/next-app-router/component";

type InterestButtonProps = {
  label: string;
  category: string;
  value: string;
};

export const InterestButton = ({ label, category, value }: InterestButtonProps) => {
  const { context } = useUniformContext();

  return (
    <button
      type="button"
      disabled={!context}
      onClick={() => context?.update({ enrichments: [{ cat: category, key: value, str: 10 }] })}
    >
      {label}
    </button>
  );
};
```

`components/interest-card.tsx`

```tsx
import type { ComponentParameter, ComponentProps } from "@uniformdev/next-app-router/component";
import { InterestButton } from "./interest-button";

type InterestCardParameters = {
  title?: ComponentParameter<string>;
  buttonLabel?: ComponentParameter<string>;
  enrichmentCategory?: ComponentParameter<string>;
  enrichmentValue?: ComponentParameter<string>;
};

export const InterestCard = ({
  parameters: { title, buttonLabel, enrichmentCategory, enrichmentValue },
}: ComponentProps<InterestCardParameters>) => (
  <div className="rounded border p-4">
    <h3>{title?.value}</h3>
    {enrichmentCategory?.value && enrichmentValue?.value ? (
      <InterestButton
        label={buttonLabel?.value ?? "I am interested"}
        category={enrichmentCategory.value}
        value={enrichmentValue.value}
      />
    ) : null}
  </div>
);
```

`cat` is the public ID of an enrichment category in Uniform. `key` is the public ID of a value in this category. `str` is the score to add.

## Notes

- **`context` can be `undefined`:** `useUniformContext` returns `undefined` until the browser Context is ready. The examples disable the button until then.
- **Keep client components small:** `"use client"` makes the component and all components that it imports client components. Put only the interactive part in a client component. Refer to [Client-side context](https://docs.uniform.app/docs/sdk/nextjs-app-router/client-context).
- **Enrichment tags on components:** if an author adds enrichment tags to a component in Canvas, the SDK updates the scores for you. You do not need code for this.
