Next.js Discord

Discord Forum

How to handle server actions with data out of formData?

Answered
Cape lion posted this in #help-forum
Open in Discord
Cape lionOP
Im wondering what would be the bes option to handle form like

const [content, setContent] = useState({});
const [state, action] = useActionState(addPostAction, {});


<form action={action}>
  <Input id="title" name="title" placeholder="My new post" />
  <Tiptap onContentChange={setContent} />
  /* first idea          
    <Input
     name="content"
     type="hidden"
     value={JSON.stringify(content)}
    /> 
  */
</form>

where there is only one real input element. I want to pass title and my state "content" to a server action.
My first idea was to create hidden input, but i dont think this is good option.

How should i handle such case?
Answered by luis_llanes
I would keep it separated but basically yes:

- useActionState would receive a wrapper function that will call the Server Action
- when you call addPostAction the form will pass FormData and React will handle this by providing the initialValue as the first argument and the FormData as the second
- your Server Action is decoupled from the FormData, but can still accept *initialValue *in case you might need it to work with it inside the Server Action
View full answer

40 Replies

Cape lionOP
Could please show it as a short example?
Like the wrapper function will be the one taking FormData:

const myFn = (formData) => {
  const title = formData.get("title");

  const myObject = { 
    title, 
    content // content is the React state at its latest value here 
  };

  action(myObject);
}
Sorry I’m on the phone lol
Let me fix it quick
Cape lionOP
Ok i think i undestand what do you mean. But how is hidden input better then modifying an actionFunciton
Is not necessary better, if you want to have more control the wrapper is the way to go.

The hidden input works for simple use cases, especially when you don’t need much security
Since the value will be visible if you inspect the element via the DevTools for example
Cape lionOP
Ok, i will try to recreate your example . Its hard to tell for now which way is better for my case. The Tiptap component is a rich-text editor, so all content would be inserted to hidden input value
@Cape lion Ok, i will try to recreate your example . Its hard to tell for now which way is better for my case. The Tiptap component is a rich-text editor, so all content would be inserted to hidden input value
I fixed the example code lol

If the content state saves a string of markup generated by the Rich Editor I don’t think there’s a problem keeping the content in the hidden input, it’s not really an antipattern
@luis_llanes Like the wrapper function will be the one taking FormData: typescript const myFn = (formData) => { const title = formData.get("title"); const myObject = { title, content // content is the React state at its latest value here }; action(myObject); }
Cape lionOP
Ok, so in my example i need to do somethin like this:
  const [state, action] = useActionState(async (previousState: AddPostState, formData: FormData) => {
    const title = formData.get("title")
     

    return addPostAction({title, content})
  }, {}); 

but my "addPostAction" cant be regular server action any longer

export async function addPostAction(
  _prevState: AddPostState,
  formData: FormData
): Promise<AddPostState> {
  const validatedFields = AddPostSchema.safeParse({
    title: formData.get("title"),
    content: formData.get("content"),
  });

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }
 // (...)


i need to modify it to take only one argument that will be type of {title: string, content: string}?
The thing is that when you pass your action to useActionState you’re basically forcing your “action” to always take initial state as the first argument, followed by whatever other arguments you wanna pass in
So you’ll have to receive two arguments:

( prevValue : InitialValueType, {title: string, content: string} )
The function wrapper still has to receive FormData because that’s what the action prop of the <form action={handler}/> passes to whatever handler
Idk if I got your question correctly, if not let me know 😬
Cape lionOP
I already had a serverActionFunction prepared for content being a formData.get('content') from hidden input

export async function addPostAction(
  _prevState: AddPostState,
  formData: FormData
): Promise<AddPostState> {
  const validatedFields = AddPostSchema.safeParse({
    title: formData.get("title"),
    content: formData.get("content"),
  });


but know i dont need a serverActionFunction, but regular function that will do exactly the same but based on my new object with {title, content}
or am i getting it tottaly wrong?
First of all you need to decide if you want to go with the action wrapper approach or the hidden input approach
Depending on that you’ll have to adapt your previous code
Cape lionOP
example of new "addPostAction" without hidden input:

  const [state, action] = useActionState(async (previousState: AddPostState, formData: FormData) => {
    const title = formData.get("title") || ''
     
    return addPostAction({title, content})
  }, {}); 


export async function addPostAction(
  postData: z.infer<typeof AddPostSchema>
): Promise<AddPostState> {
  const validatedFields = AddPostSchema.safeParse({
    title: postData.title,
    content: postData.content,
  });

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }

  const { title, content } = validatedFields.data;

  try {
    const response = await fetch(`${process.env.SERVER_URL}/api/posts/add`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ title, content }),
    });

    if (!response.ok) {
      const errorData = await response.json();
      return {
        message: errorData.message || "Addin post failed",
      };
    }
  } catch (error) {
    return {
      message: "An error occurred while logging in",
    };
  }

  redirect("/dashboard/posts");
}
@Cape lion I already had a serverActionFunction prepared for content being a formData.get('content') from hidden input jsx export async function addPostAction( _prevState: AddPostState, formData: FormData ): Promise<AddPostState> { const validatedFields = AddPostSchema.safeParse({ title: formData.get("title"), content: formData.get("content"), }); but know i dont need a serverActionFunction, but regular function that will do exactly the same but based on my new object with {title, content} or am i getting it tottaly wrong?
Do you want the {title, content} processing to happen on the server? Then it still needs to be a server action.

Server Actions can take whatever parameters you want, after all they’re functions like any other

When you pass the Server Action to an “action prop” in a form it’s the form the one that provides the FormData that’s why you need to adapt the Server Action to take FormData
Idk if that makes sense?
Server Actions aren’t tied to FormData, in fact they’re not tied to any specific set of arguments, it all depends on the context on which you’re using it
Cape lionOP
I am kinda confused 😄
Did you think server actions are tied to FormData?
Cape lionOP
My previous server action with arguments
export async function addPostAction(
  _prevState: AddPostState,
  formData: FormData
): Promise<AddPostState>

was tied to FormData and i thought every server action need to be build like this
Oh no, the parameters that server actions take can be anything
Cape lionOP
In case with hidden input it would work properly, cuz formData would already have a value of "content" from hidden input.

Currently working on version wihtout hidden input.

My form component
export default function Login() {
  const [content, setContent] = useState({});
  const [state, action] = useActionState(
    async (previousState: AddPostState, formData: FormData) => {
      const title = formData.get("title") || "";
      return addPostAction({ title, content: JSON.stringify(content) });
    },
    {}
  );

  return (
    <div className="justify-items-center min-h-screen font-[family-name:var(--font-geist-sans)] py-4">
      <div className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
        <form action={action}>
          <Card className="w-[350px]">
            <CardHeader>
              <CardTitle>Add post</CardTitle>
            </CardHeader>
            <CardContent>
              <div className="grid w-full items-center gap-4 pb-6">
                <div className="flex flex-col space-y-1.5">
                  <Label htmlFor="title">Title</Label>
                  <Input id="title" name="title" placeholder="My new post" />
                </div>
                <div className="flex flex-col space-y-1.5">
                  <Label htmlFor="content">Content</Label>
                  <Tiptap onContentChange={setContent} />
                </div>
              </div>
            </CardContent>
            <CardFooter className="flex justify-end">
              <Button type="submit">Add</Button>
            </CardFooter>
          </Card>
        </form>
      </div>
    </div>
  );
}
Like I said, when you call a server action inside an “action prop” in a form you’re passing FormData as an argument that’s why you need to receive Form Data in your server actions

Look maybe this helps

<form action={myAction}/>

is the same as:

<form action={ (formData) => myAction(formData) }/>

Server Actions can take any type of arguments it’s not a contract you need to follow. In the case of an action you’re passing FormData that’s it
And apart from that, when you pass an action to useActionState you’re now tied to the way useActionState handles the action which is passing initialState as the first argument, the rest of the arguments will depend on what you’re passing to it
Cape lionOP
Ok, understand it now
Cape lionOP
But if i want to keep useActionState way of handling action - is my approach ok?
Yes, any approach you follow can make use of useActionState, just have in mind that the second parameter your server actions takes will depend on whether you’re directly passing the action to the “action prop” or calling the action in the wrapper we were taking about
Let me get on my PC and I’ll try to demonstrate how, its hard on my phone lol
Cape lionOP
I'll get back to this topic tomorrow—I need some sleep. Thanks for your help! I'll check your messages first thing in the morning.
I would keep it separated but basically yes:

- useActionState would receive a wrapper function that will call the Server Action
- when you call addPostAction the form will pass FormData and React will handle this by providing the initialValue as the first argument and the FormData as the second
- your Server Action is decoupled from the FormData, but can still accept *initialValue *in case you might need it to work with it inside the Server Action
Answer
https://github.com/llanesluis/template/blob/main/src/app/page.client.tsx

Did a quick demo with a very simple use case following exactly the same approach. Component is mounted in the root page (project is a template where I dump code and patterns lol don't take it very seriously)
Cape lionOP
Thank you! Everything is clear now
You’re welcome, I’m glad! 😉
:meow_coffee: