Skip to content

server action closures should capture member access paths, not whole objects #1171

@hi-ogawa

Description

@hi-ogawa

When a server action closes over a property access, the whole root object is serialized and sent over the network, even if only one leaf value is needed.

Example

function Page() {
  const config = getConfig() // large object, may contain non-serializable values

  async function action() {
    'use server'
    return config.api.key  // only this leaf is used
  }
}

Currently the entire config object is captured:

const action = register(hoistedAction).bind(null, config)

export async function hoistedAction(config) {
  'use server'
  return config.api.key
}

This means:

  • More data is serialized into the RSC payload than necessary
  • If config contains non-serializable values (functions, class instances, etc.), the action fails at runtime — even though config.api.key itself is a plain serializable value

Expected behavior

Only the accessed path should be captured, so the action works as long as the accessed value is serializable, regardless of the rest of the object.

For example, this could be achieved by binding only the needed subtree:

const action = register(hoistedAction).bind(null, { api: { key: config.api.key } })

export async function hoistedAction(config) {
  'use server'
  return config.api.key
}

Notes

Next.js already handles this correctly by tracking member access chains rather than just the root identifier.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions