Next.js

Fix Functions cannot be passed directly to Client Components

Read the caret line to find the exact prop, then keep that function on the client side of the boundary: four reproduced shapes on Next.js 16.4.0, and why "use server" rarely fixes it.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

You render a page in the Next.js App Router and the server log stops on a 500 with this message. The Stack Overflow question behind this page (68 votes, over 100,000 views) hit it with a data-grid column whose renderCell was an arrow function, built in a Server Component and handed to a client table. The accepted answer there points to "use server"; for that case it is the wrong tool, and the reproduction below shows why.

TL;DR#

A Server Component passed a plain JavaScript function as a prop (or nested inside a prop) to a Client Component. Props that cross that boundary are serialised, and a plain function cannot be. The caret line under the error names the exact prop. Fix it by defining that function inside a 'use client' file and passing only data from the server; pass a JSX element instead of a component; for next/image, configure a loader file instead of a loader prop. Mark it as a Server Function only when it is genuine async server work.

text
⨯ Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". Or maybe you meant to call this function rather than return it.
  {field: "pk", headerName: "", renderCell: function renderCell}
                                            ^^^^^^^^^^^^^^^^^^^

That is the message reproduced on Next.js 16.4.0, with react and react-dom 19.3.0 installed, in the official node:24-bookworm-slim Docker image, on 13 October 2026. The question was asked on Next.js 13.0.5, and the message it quotes ends after "use server"; the current one adds a sentence about calling the function.

Why it happens#

In the App Router, pages and layouts are Server Components by default (see the Server and Client Components docs). When a Server Component renders a Client Component, React does not hand it live JavaScript objects: it writes the props into a serialised stream that the browser reads. The React reference for 'use client' states the rule:

Prop values passed from a Server Component to Client Component must be serializable.

Its list of supported values includes primitives, arrays, plain objects, Date, promises, JSX elements and Server Functions. Its list of unsupported values starts with functions that are neither exported from a client module nor marked with 'use server'. An arrow function inside a column definition is exactly that, and so is a component function passed where an element was expected.

The check is not shallow. In the reproduction, the function sat two levels down, in an object inside an array in the columns prop, and React still found it: the caret line printed the object that holds it and underlined the key. That line is the fastest way to locate the culprit in a large tree.

The wording you get depends on the prop name. In React's Flight server source, a function whose property name starts with "on" plus a capital letter throws the "Event handlers cannot be passed to Client Component props" error; a function under any other prop name falls through to the "Functions cannot be passed directly" error (a function passed as children has its own message in development). The reproduction confirms both branches on the same client table: renderCell produced this page's message, onRowClick produced the event-handler one.

Fix#

1. Read the caret line and find the prop#

The second and third lines of the error print the object or element that holds the function, with carets under the key. In the four reproduced shapes the key was renderCell (a column definition), icon (a component passed as a prop), loader (a next/image loader) and onRowClick (the event-handler variant). Open the Server Component that builds that prop.

2. Move the function to the client side of the boundary#

This is the fix for the original question. The client table that broke in the reproduction is an ordinary 'use client' component that calls renderCell for each cell:

jsx
// app/table.js
'use client'
 
export default function Table({ rows, columns, onRowClick }) {
  return (
    <table>
      <tbody>
        {rows.map((row) => (
          <tr key={row.pk} onClick={() => onRowClick?.(row)}>
            {columns.map((col) => (
              <td key={col.field}>
                {col.renderCell ? col.renderCell({ row }) : row[col.field]}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  )
}

The failing page built columns, including the arrow function, in the Server Component. Move the column definitions into a small client wrapper, so the function never crosses the boundary:

jsx
// app/fixed/matches-table.js
'use client'
 
import Link from 'next/link'
import Table from '../table'
 
const columns = [
  { field: 'home', headerName: 'Home' },
  {
    field: 'pk',
    headerName: '',
    renderCell: (params) => (
      <Link href={`/matches/${encodeURIComponent(params.row.pk)}`}>Details</Link>
    ),
  },
]
 
export default function MatchesTable({ rows }) {
  return <Table rows={rows} columns={columns} />
}

The page stays a Server Component, fetches or builds the rows, and passes only data:

jsx
// app/fixed/page.js
import MatchesTable from './matches-table'
 
const rows = [{ pk: 1, home: 'Arsenal' }, { pk: 2, home: 'Lyon' }]
 
export default function Page() {
  return <MatchesTable rows={rows} />
}

The same move fixes a carousel whose formatter props were set by the page, a case covered in the react-responsive-carousel "Super expression" fix.

3. Pass an element, not a component#

The second half of the message, "Or maybe you meant to call this function rather than return it", fits this shape. Passing icon={Star} sends the component function itself; passing icon={<Star />} sends a rendered element, which is serialisable. The client card accepts either:

jsx
// app/card.js
'use client'
 
export default function Card({ title, icon: Icon }) {
  return (
    <div>
      {typeof Icon === 'function' ? <Icon /> : Icon} {title}
    </div>
  )
}

The fixed page renders the icon on the server and passes the element:

jsx
// app/icon-fixed/page.js
import Card from '../card'
import Star from '../star'
 
export default function Page() {
  return <Card title="Favourites" icon={<Star />} />
}

An SVG imported through a loader that turns it into a component produces this same shape on next/image; the SVG import fix covers that case.

4. For a next/image loader, use a loader file#

The third Stack Overflow answer reported the error with loader={function} on an image rendered by a server page. The reproduction shows the loader prop rejected at the Client Component boundary, with the same message, when a Server Component sets it on Image. The next/image reference offers the loader file option, which configures every image without a prop. In next.config.mjs:

js
// next.config.mjs
export default {
  images: {
    loader: 'custom',
    loaderFile: './cdn-loader.js',
  },
}

The loader file carries the 'use client' directive, as in the docs example:

js
// cdn-loader.js
'use client'
 
export default function cdnLoader({ src, width, quality }) {
  return `https://cdn.example.com${src}?w=${width}&q=${quality || 75}`
}

The page then drops the loader prop:

jsx
// app/img-fixed/page.js
import Image from 'next/image'
 
export default function Page() {
  return <Image src="/shirt.png" width={350} height={350} alt="Shirt" />
}

The alternative, also shown in the docs, is to render the Image with its loader inside a 'use client' component.

5. Use a Server Function only for real server work#

If the function deletes a record or writes to a database, and the client should trigger it, then it belongs on the server. Make it async and mark it with 'use server', and React passes a reference instead of the function body:

jsx
// app/action/page.js
import DeleteButton from './delete-button'
 
async function deleteMatch(pk) {
  'use server'
  console.log('deleting match', pk)
  return { ok: true }
}
 
export default function Page() {
  return <DeleteButton pk={1} deleteMatch={deleteMatch} />
}
jsx
// app/action/delete-button.js
'use client'
 
export default function DeleteButton({ pk, deleteMatch }) {
  return <button onClick={() => deleteMatch(pk)}>Delete</button>
}

In the reproduction this page rendered with a 200. The directive does not make the page a Server Component (it already is one); the difference between the two directives is set out in use client vs use server.

Verify the fix#

Request the page and watch the server log. In the reproduction every failing route returned 500 and every fixed one 200, on the same dev server:

text
== GET /columns -> 500
== GET /handler -> 500
== GET /action -> 200
== GET /fixed -> 200
== GET /icon -> 500
== GET /icon-fixed -> 200
== GET /img -> 500
== GET /img-fixed -> 200

With the loader file in place, the rendered image pointed at the custom loader's URL rather than the built-in optimiser:

text
== GET /img-fixed -> 200 <img alt="Shirt" loading="lazy" width="350" height="350" decoding="async" data-nimg="1" style="color:transparent" srcSet="https://cdn.example.com/shirt.png?w=384&amp;q=75 1x, https://cdn.example.com/shirt.png?w=750&amp;q=75 2x" src="https://cdn.example.com/shirt.png?w=750&amp;q=75"/>

next dev reports only the pages you request. next build prerenders static pages and, in the reproduction, stopped at the first failing route, so a production build can show a different page first (here the event-handler variant on /handler):

text
Error occurred prerendering page "/handler". Read more: https://nextjs.org/docs/messages/prerender-error
Error: Event handlers cannot be passed to Client Component props.
  {rows: [...], columns: [...], onRowClick: function onRowClick}
                                            ^^^^^^^^^^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.
    at ignore-listed frames {
  digest: '621319535'
}
Export encountered an error on /handler/page: /handler, exiting the build.

Fix it, rebuild, and repeat until the build completes.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Why "use server" does not rescue a render function#

The accepted answer suggests marking the function with "use server". A render function such as renderCell returns JSX synchronously, for every cell, in the browser. The React reference for 'use server' explains that the directive turns the function into a network call, which is why it only accepts async functions. Adding the directive to the synchronous renderCell in the reproduction swapped the runtime error for a compile error:

text
⨯ ./app/columns-use-server/page.js:10:28
Error: Server Actions must be async functions.
   8 |     {
   9 |       field: 'pk',
> 10 |       renderCell: function renderCell(params) {
     |                            ^^^^^^^^^^
  11 |         'use server'
  12 |         return <Link href={`/matches/${params.row.pk}`}>Details</Link>
  13 |       },

The same reference explains that calling a Server Function from the client sends a network request and returns a serialised result. A callback that the table calls for every cell, and whose output is a link to render, is client work. Keep render callbacks on the client (step 2).

The error.js shape from the second answer#

The second answer on the question reports the message coming from an error.js file that lacked 'use client'. On Next.js 16.4.0 the same mistake no longer reaches this runtime message: the compiler stops first with a dedicated error.

text
Error: app/boom/error.js must be a Client Component. Add the "use client" directive the top of the file to resolve this issue.

Adding 'use client' as the first line removed that error, and the route then surfaced the page's own thrown error instead. The error.js reference states the requirement in a comment on its examples: error boundaries must be Client Components.

When it turns into "Event handlers cannot be passed"#

Rename renderCell to something like onRowClick and the message changes to "Event handlers cannot be passed to Client Component props", because of the prop-name test above. The cause and the remedy are the same; the event handlers fix walks through the onClick-specific shapes. If the function is in a root layout and the build fails on /_not-found, the Next.js 16 /_not-found prerender fix covers that path. And if a Date is what will not cross the boundary in a Pages Router getStaticProps or getServerSideProps, that is a separate serialisation error.

The rule underneath all these shapes is the same: everything a Server Component hands to a Client Component travels as data. Keep functions that render or react to the user in the client module that uses them, send rows, ids and elements across the boundary, and reserve 'use server' for async work that genuinely has to run on the server.

Further reading: Unrecognized feature: 'interest-cohort' on GitHub Pages. Also see Fix "changing an uncontrolled input to be controlled". Also see Fix Unsupported Server Component type: undefined. Also see Set the request's mode to 'no-cors'? What to do instead.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote