Fetch data in a component

Developer preview

This feature is in developer preview. Use with caution as it may change unexpectedly. For more information, contact us.

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.

  • A Next.js app with the SDK. Refer to Next.js App Router SDK.
  • A component definition productCard with a text parameter productId.

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

lib/products.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.

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

lib/products.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.

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

components/product-card.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> ); };

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

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.

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

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 sourceRead it inExample
A parameterparameters.productId?.valueA product card on a landing page
A dynamic inputcontext.dynamicInputs.productIdA 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.

  • 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.
  • 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.