Recently I have relied quite heavily on a Next.js app directory architecture, where data is fetched from a database in the server component and then passed to client for further clientside processing or display.
app/products/[id]/ - page.tsx (server component) - Product.tsx (client component with clientside-enriched visualization)
As nice as this is to move some processing and rendering to serverside and thus achieve more SEO- and slow-connection-friendly HTML output, it doesn’t directly offer any solutions for passing type-safe data to client.
Here I describe the pattern I’ve found simple and straightforward for type-safely passing data to client and separating data fetching and usage concerns.
Data pattern
The pattern involves adding a new data.ts
file, which is responsible for 1. serverside data fetching and 2. providing types for fetched data.
app/products/[id]/ - page.tsx - data.ts (serverside data loading utility and shared type for data result) - Product.tsx
data.ts
import "server-only";import { supaClient } from "@/app/supaClient";export function load(productId: string) { return supaClient() .from("products") .select("name, price") .eq("id", productId);}export type Data = Awaited<ReturnType<typeof load>>;
page.tsx
import { Product } from "./Product";import { load } from "./data";export default async function Page({ params: { id } }) { const data = await load(id); return ( <div className="page-panel"> <p>This page contains visualization for {data.name}</p> <Product id={id} data={data} /> </div> );}
Product.tsx
"use client";import type { Data } from "./data";export function Product({ id, data }: { id: string; data: Data }) { const formattedPrice = new Intl.NumberFormat( undefined, { style: 'currency', currency: 'USD' } ).format(data.price); return ( <section> <h2>{data.name}</h2> <p>price: {formattedPrice}</p> </section> );}
This pattern both simplifies implementation of page.tsx
and makes the app more type-safe with up-to-date and non-duplicated type definitions for page data. Additionally, it works well with Next.js’ own data preload pattern.