# Recipe: fetch data in a component with the Next.js App Router SDK

> Get more data on the server in a Uniform component: an async server component, Next.js fetch cache tags, a Suspense fallback, and dynamic inputs from the project map.

Source: https://docs.uniform.app/docs/sdk/nextjs-app-router/recipes/data-fetching

**Goal:** show data from a different API in a Uniform component. For example, an author enters a product ID, and the component gets the name and the price of the product from your commerce API.

## Prerequisites

- A Next.js app with the SDK. Refer to [Next.js App Router SDK](https://docs.uniform.app/docs/sdk/nextjs-app-router).
- A component definition `productCard` with a text parameter `productId`.

## Step 1: Make the data function

Make a function that gets the product. Give the fetch a cache tag, so that you can revalidate the product later:

`lib/products.ts`

```ts
export type Product = {
  id: string;
  name: string;
  description: string;
  price: number;
};

export async function getProduct(id: string): Promise<Product | null> {
  const response = await fetch(`https://api.example.com/products/${encodeURIComponent(id)}`, {
    cache: "force-cache",
    next: { revalidate: 3600, tags: [`product:${id}`] },
  });

  if (!response.ok) return null;

  return (await response.json()) as Product;
}
```

`cache: "force-cache"` keeps the response in the Next.js data cache. Without this option, Next.js 16 does not keep the response in the data cache. `revalidate: 3600` sets a maximum age of one hour.

### With Cache Components

If you set `cacheComponents: true` in `next.config.ts`, use the `"use cache"` directive in place of the fetch options:

`lib/products.ts`

```ts
import { cacheLife, cacheTag } from "next/cache";

export type Product = {
  id: string;
  name: string;
  description: string;
  price: number;
};

export async function getProduct(id: string): Promise<Product | null> {
  "use cache";
  cacheLife("hours");
  cacheTag(`product:${id}`);

  const response = await fetch(`https://api.example.com/products/${encodeURIComponent(id)}`);

  if (!response.ok) return null;

  return (await response.json()) as Product;
}
```

Use this version only with Cache Components. The `"use cache"` directive works only when `cacheComponents` is `true`.

## Step 2: Make an async server component

A Uniform component is a React Server Component. Thus it can be `async`, and it can wait for the data:

`components/product-card.tsx`

```tsx
import type { ComponentParameter, ComponentProps } from "@uniformdev/next-app-router/component";
import { getProduct } from "@/lib/products";

type ProductCardParameters = {
  productId?: ComponentParameter<string>;
};

export const ProductCard = async ({
  parameters: { productId },
  context,
}: ComponentProps<ProductCardParameters>) => {
  const id = productId?.value;

  if (!id) {
    return context.isContextualEditing ? <p>Enter a product ID.</p> : null;
  }

  const product = await getProduct(id);

  if (!product) return null;

  return (
    <article className="rounded border p-4">
      <h3>{product.name}</h3>
      <p>{product.description}</p>
      <p>${product.price.toFixed(2)}</p>
    </article>
  );
};
```

## Step 3: Register the component with a Suspense fallback

Without a `Suspense` boundary, the page waits for the slowest component. Add the `suspense` option in `resolveComponent`. The SDK then puts the component in a React `Suspense` boundary. The page shows the fallback first, and the product streams in when its data is ready:

`components/resolveComponent.tsx`

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

import { PageComponent } from "./page";
import { ProductCard } from "./product-card";
import { ProductDetail } from "./product-detail";

const componentMap: Record<string, ResolveComponentResult["component"]> = {
  page: PageComponent,
  productDetail: ProductDetail,
};

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

const ProductCardSkeleton = () => <div className="h-32 animate-pulse rounded bg-gray-200" />;

export const resolveComponent: ResolveComponentFunction = ({ component }) => {
  if (component.type === "productCard") {
    return {
      component: ProductCard,
      suspense: { fallback: ProductCardSkeleton },
    };
  }

  return { component: componentMap[component.type] ?? NotFound };
};
```

`fallback` is a component, not an element. The SDK renders it with no props.

> **Note:**
>
> In Canvas, each change of the author refreshes the page. A component with a `Suspense` boundary then shows its fallback again. Use `suspense` only for components that are slow. Refer to [Streaming with Suspense](https://docs.uniform.app/docs/sdk/nextjs-app-router/caching#streaming-with-suspense).

## Step 4 (optional): Use dynamic inputs from the project map

A project map node can have a dynamic segment, for example `/products/:productId`. All product pages then use one composition. The component reads the value of the segment in `context.dynamicInputs`, not in a parameter:

`components/product-detail.tsx`

```tsx
import type { ComponentProps } from "@uniformdev/next-app-router/component";
import { notFound } from "next/navigation";
import { getProduct } from "@/lib/products";

export const ProductDetail = async ({ context }: ComponentProps) => {
  // The value of the :productId segment of the project map node /products/:productId
  const id = context.dynamicInputs.productId;

  if (!id) return null;

  const product = await getProduct(id);

  if (!product) notFound();

  return (
    <section>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </section>
  );
};
```

For the URL `/products/shoe-42`, `context.dynamicInputs.productId` is `"shoe-42"`. The example in step 3 registers `ProductDetail` without `suspense`. A component in a `Suspense` boundary can stream after Next.js sends the status code. Thus do not use `suspense` for a component that calls `notFound()`.

Use a parameter when the author selects the data. Use a dynamic input when the URL selects the data.

| Data source | Read it in | Example |
| --- | --- | --- |
| A parameter | `parameters.productId?.value` | A product card on a landing page |
| A dynamic input | `context.dynamicInputs.productId` | A product detail page for each product |

`dynamicInputs` comes from the Route API. A composition from `resolveCompositionById` or `resolveCompositionBySlug` has no dynamic inputs, so `context.dynamicInputs` is an empty object.

## Notes

- **Revalidation:** to get new product data before the cache expires, call `revalidateTag("product:shoe-42", "max")` in a route handler or a Server Action. For example, call it from a webhook of your commerce system. Refer to the [Next.js `revalidateTag` reference](https://nextjs.org/docs/app/api-reference/functions/revalidateTag).
- **Preview:** Canvas preview uses Next.js draft mode. In draft mode, Next.js does not use the fetch cache. Thus authors always see the current product data.
- **The same fetch in more than one component:** Next.js does the same `GET` request one time for each render, also without a cache. Thus two components can call `getProduct` with the same ID.
- **Change the composition data:** to change parameters or slots before the components render, for example with Uniform enhancers, resolve the composition on the page. Refer to [Change the composition data before render](https://docs.uniform.app/docs/sdk/nextjs-app-router/resolving-compositions#change-the-composition-data-before-render).
