Next.js Discord

Discord Forum

Can't make Work properly a server action from a client form

Unanswered
Rex posted this in #help-forum
Open in Discord
RexOP
Hi all!
i'm losing my mind on such an easy thing.
i've also done a TodoApp just for test and i can't make a form action called from a client form work.



the code is pretty basic, i'm also using an in memory array. I've also created a server side form and works as expected, but not the client one.

here is the code for the client form:


function FormClient( {} : Props ){

const {pending} = useFormStatus()
const ref = useRef<HTMLFormElement>(null)

return (
<form ref={ref} action={ async formData => {
await pushTodo(formData)
ref.current?.reset();
}} className="flex gap-y-3 flex-col">
<label htmlFor="todo">{'Action from client'}</label>
<input className="text-black" type="text" id="todo" name="todo" required />
<button className="p-2 bg-teal-900 rounded-lg" type="submit" aria-disabled={pending} disabled={pending}>
{'Add Todo'}
</button>
</form>
);
}

and this is the server action, in a separate file of course as stated in the docs.

export const pushTodo = async (data: FormData) => {
const d = data.get("todo") as string

await new Promise( resolve =>
setTimeout(() => {
resolve(todos.push(d))
}, 500))
console.log('calling addTodo with: ', d)
revalidatePath("/")
return {message: "done"}
}

Here the crazy part ( for me, maybe i'm missing something):

if i call that action from a server-side form it work, and than start working even the client-side form

if i create a separate "inline" action for the server form, this will work fine but the action called from the client form ( the one here in the code, in his separate file ) will NOT work

any help will be very appreciated, this is driving me crazy

thanks

14 Replies

dont worry man, we've all been there. Did you forget the 'use server'; part or just omit it?
And the use client also
RexOP
Sorry maybe was not copied, I’ve double and triple checked, in the client form I’ve set the “use client” as well as in the action the “use server” ( both at the top of the file)


The weird behaviour is that when I click the button it actually call the action, I’ve added some console.log in the action, but the revalidatePath is not triggered somehow.

Using the Network tab I can see the post but in the payload is missing the actionId.
( while using the server side form the actionId is there as expected in the request payload).
RexOP
Update:

If I pass the action as prop from a server component to the client component it works!

But in a real case scenario I don’t like to pro drill a function from server to client component.

I can’t find in the docs that this is the intended practice..

Am I Wrong ?
I dont understand what's going on. I'm going to share with you a simple form component I have that opens in a dialog (uploaded file).

Here some important snippets
'use client'

import { FormSubmit } from '@/components/form/form-submit'
import { saveOrUpdate } from '@/actions/companies'


export default function CompanyForm ({ company, children }: CompanyFormProps) {
  // Same as you, but I extracted in a const so the HTMX looks more compact 
  const onSubmit = async (id: string | null, formData: FormData) => {
    const name = formData.get('name') as string
    const response = await saveOrUpdate({ name }, id)

    let errors: string[] = []
    if (response?.type === 'error' && response?.message) {
      errors.push(response.message)
      setErrors(errors)
    }

    if (response?.type === 'success') {
      toaster.send(response)
      closeRef.current?.click()
    }
  }

  const submitFn = company
    ? onSubmit.bind(null, company.id)
    : onSubmit.bind(null, null)

  return (
        <form action={submitFn}> 
          <FormErrors errors={errors} />
          <div className='flex flex-col gap-3'>
            <Input
              name='name'
              placeholder='Nome da empresa'
              defaultValue={company?.name}
            />
          </div>
           // other stuff

          <DialogFooter className='mt-24 flex flex-row gap-1'>
            <DialogClose
              ref={closeRef}
              asChild
              onClick={() => setErrors(undefined)}
            >
              <Button variant='destructive' size='lg'>
                Cancelar
              </Button>
            </DialogClose>
            <FormSubmit size='lg'>Salvar</FormSubmit>
          </DialogFooter>
        </form>
Oh so the problem is that your revalidatePath is not working right?
This is your code:

    await new Promise( resolve =>
    setTimeout(() => {
        resolve(todos.push(d))
    }, 500))
    console.log('calling addTodo with: ', d)
    revalidatePath("/")
    return {message: "done"}


Please try this:
Move revalidatePath("/") inside your timeout after 500ms.
My guess is that you're revalidating the path before you actually add the data on the todos list.
RexOP
thanks for sharing your thoughts, however i've checked again, moved the revalidatePath just after the call to fetch, but it doesn't work..
@Rex Update: If I pass the action as prop from a server component to the client component it works! But in a real case scenario I don’t like to pro drill a function from server to client component. I can’t find in the docs that this is the intended practice.. Am I Wrong ?
we can pass the action as props to client component. Sometime I had to do this if the action use some external packages otherwise I got webpack error
Bighead carp
@Rex maybe show the side-by-side codeblocks of what does work and what doesnt. specifically what the change is that stops things from working
and make sure wrap your code with three of these characters: `
const example = 2;