Bridging Server and Client Components
Cover photo by Tyler on Unsplash
Why does this fail?
Look at this code:
import { MyClientButton } from './MyClientButton';
export default async function Page() { const data = await getDashboardData(); return <MyClientButton onClick={() => console.log(data)} />;}What is wrong with this? I see people try this all the time. You think you can just pass the server-side data directly into a client component event handler. It won’t work. The function console.log(data) is executed on the browser, but data only exists on the server. The reference simply dies in transit.
The naive approach
Commonly, developers try to serialize everything into JSON strings or prop-drill way too much. They might try to pass the data as a serialized prop like <MyClientButton initialData={JSON.stringify(data)} />. While that technically works, it gets messy fast. If your data structure changes, you are stuck maintaining manual serialization logic everywhere. That is a maintenance nightmare.
The real bridge
Instead of trying to pass logic across the boundary, separate the concerns. Treat the server as the data source and the client as the event handler. If the client needs the data to perform an action, the server should provide an API endpoint or a Server Action. Here is how I prefer to do it.
'use client';
export function MyClientButton({ action }) { return <button onClick={action}>Click me</button>;}
// page.jsimport { MyClientButton } from './MyClientButton';
export default async function Page() { async function serverAction() { 'use server'; const data = await getDashboardData(); console.log(data); }
return <MyClientButton action={serverAction} />;}This works because Server Actions are functions you can pass as props. It feels like magic, but it is just a stable way to call server logic from a client component without manually writing REST endpoints (which I find tedious to wire up for every little action).
Is this always the right way?
“But Namir, this makes my components tightly coupled to the server!”
I hear this concern often. It is fair. If you are building a generic UI library that needs to be framework-agnostic, you absolutely should not be using Server Actions. You would be better off with simple props and a standard API. However, for most application-level code, the coupling is a feature, not a bug. It saves you from maintaining a separate layer of glue code.
Keep it simple
I struggle with over-engineering this part of my apps. Sometimes I spend an hour trying to abstract the communication layer, only to realize I could have just passed a simple async function prop. Don’t be like me.
One caveat to keep in mind, you cannot pass complex data types like Classes or Promises through these props. It has to be serializable. If you find yourself trying to pass a database connection object, stop (please, just stop). That should stay tucked away in your data access layer.
Do you find Server Actions make your codebase cleaner, or do they just move the complexity around?