vercel / next.js

The React Framework
https://nextjs.org
MIT License
125.49k stars 26.8k forks source link

Docs: How to pass pass down a Server Action to a Client Component as a prop in typescript #54156

Closed bfovez closed 2 months ago

bfovez commented 1 year ago

What is the improvement or update you wish to see?

The docs here https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#props shows how to pass pass down a Server Action to a Client Component as a prop.

But as soon as we use typescript instead of plain javascript, things must be typed:

example-server-component.tsx:

import { ExampleClientComponent } from "./example-client-component"

type Props = { id: string }

function ExampleServerComponent({ id }: Props) {
  async function updateItem(data: FormData) {
    "use server"
    modifyItem({ id, data })
  }

  return <ExampleClientComponent updateItem={updateItem} />
}

example-client-component.tsx:

"use client"

type Props = {
  updateItem: (data: FormData) => Promise<void>
}

export function ExampleClientComponent({ updateItem }: Props) {
  return (
    <form action={updateItem}>
      <input type="text" name="name" />
      <button type="submit">Update Item</button>
    </form>
  )
}

Then, we get an error from typescript in [the client](example-client-component.tsx: Props must be serializable for components in the "use client" entry file, "updateItem" is invalid.ts(71007)

However, it somehow works anyway.

The docs should be more explicit about how the things must be properly done with typescript.

Is there any context that might help us understand?

How to implement the example of this part of the docs pass down a Server Action to a Client Component as a prop with typescript.

Does the docs page already exist? Please link to it.

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#props

NEXT-1524

balazsorban44 commented 1 year ago

Thanks, this is actually a bug with the TypeScript plugin, not the documentation. We will look into it!

SamHep0803 commented 1 year ago

Is there any update on this? Still seems to be an issue in the latest version

lukeberry99 commented 11 months ago

Any update on this? Still getting it

JoseVSeb commented 10 months ago

Is there a way to disable the rule ts(71007) globally?

It's more troublesome than it's worth. There's already a runtime exception thrown if anything other than a server action is passed as prop to a client component from a server component. Having this rule enabled seems to make it opinionated that you can't have components with "use client" to have optional functions as props, which you may choose to pass depending on whether you use it in server component or another client component.

ADTC commented 8 months ago

I noticed that if you make it optional, the warning goes away.

updateItem?: (data: FormData) => Promise<void>
// OR
// Note: Remember the needed bracket enclosure, otherwise the meaning is different.
updateItem: ((data: FormData) => Promise<void>) | undefined

But still, it's confusing. Is there a way to distinguish server actions from regular functions? Like by using a hypothetical ServerAction type?

// HYPOTHETICAL CODE - DO NOT COPY
import type { ServerAction } from 'next'
...
updateItem: ServerAction<(data: FormData) => Promise<void>>
// HYPOTHETICAL CODE - DO NOT COPY

This can help to type-check that only server actions are passed to the client as props (not any vanilla function).

Also, the docs don't have a TypeScript example for the part where it says "You can also pass a Server Action to a Client Component as a prop".

redbmk commented 7 months ago

I found a workaround until the bug is fixed without making it optional. It's still not ideal especially since I couldn't get it to work with generics, but as a stopgap you could create many of these for any flavor of action (FormData as input returning void, specific params returning some optional error message, etc)

I'm not sure why this works because inspecting the end type still shows an async function ((data: FormData) => Promise<void>), but typescript is cool with it now.

src/actions/index.ts

"use server";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function serverAction(data: FormData): Promise<void> {
  throw new Error("Not implemented");
}

export type ServerAction = typeof serverAction;

src/components/Something.tsx

"use client"

import { ServerAction } from '@/actions'
// or 
// import type { ServerAction } from '@/actions'

export const Something = ({ action }: { action: ServerAction }) => (
  <form action={action}>
    <input name="something" />
  </form>
)
irvin93d commented 6 months ago

I found a workaround until the bug is fixed without making it optional. It's still not ideal especially since I couldn't get it to work with generics, but as a stopgap you could create many of these for any flavor of action (FormData as input returning void, specific params returning some optional error message, etc)

This seems to work with generics.

export type ServerAction<T> = ((data: T) => Promise<void>) & Function

// in client component
export const TheForm = ({ myAction }: { myAction: ServerAction<{ hey: string }> }) => {
  return (
    <form action={myAction.bind(undefined, { hey: 'you' })}>
      <button>Submit</button>
    </form>
  )
}

// in server component
<TheForm
  myAction={async data => {
    'use server'
    console.info(data)
  }}
/>
TimotiusAnrez commented 3 months ago

is there any update regarding this issue? I found that this work around resulting in this error message

index.d.ts(3202, 9): The expected type comes from property 'action' which is declared here on type 'DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>'

Server action

image

Client Component image

Is there a proper way to resolve this?

eps1lon commented 3 months ago

The current plan by the React team is to establish the pattern of using the Action suffix as a hint that a prop is an Action. We're going to leverage that convention in the Next.js TypeScript plugin: Screenshot 2024-06-26 at 14 32 50

It's not perfect since you could still pass a function that's not a Server function. But we still have runtime type-checking to catch that.

ScreamZ commented 2 months ago

Looks good

github-actions[bot] commented 2 months ago

This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.