Sunday, 28 July 2024
Next.js Server Actions - how do I send complex data
[Next.js](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#forms) has some extraordinary integration between front- and back-ends. It takes some thinking about, but the thing that stumped me the longest was getting relatively complex objects (i.e. not just simple strings) across the "Server Actions" boundary.
## The Rules
There are lots of [rules](https://react.dev/reference/rsc/use-server#serializable-parameters-and-return-values) about __what__ you can send. Basically, it has to be serializable, which makes sense. But *how* do you send these things?
Unfortunately, most of the documentation limits itself to [trivially-simple](https://react.dev/reference/rsc/use-server#server-actions-in-forms) examples; like [a single simple string](https://react.dev/reference/react-dom/components/form#handle-form-submission-with-a-server-action)
My specific use case was trying to get an object that contained an array of very simple objects across the boundary:
```
type DTO = {
id: number;
foo: string;
people: Person[];
}
type Person = {
name: string;
age: number;
}
const dataToSend: DTO = {
id: 123,
foo: "bar",
people: [
{
name: "Tom",
age: 12,
},
{
name: "Dick",
age: 45,
},
{
name: "Harry",
age: 78,
},
]
];
```
While the order of the objects within the array was unimportant, it was crucial that the elements of the object could be rehydrated (for want of a better expression) correctly on the server side - i.e. with the `Harry`-is-`78` connection intact, guaranteed.
## On the client (version 1)
This should not be too hard, I thought to myself. I mean, sending form contents was pretty-much the original "interactive internet" mechanism; `/cgi-bin` and all that old stuff. I found a useful page that explained a [decent mechanism](https://mattstauffer.com/blog/a-little-trick-for-grouping-fields-in-an-html-form/) to serialize arrays, which I implemented like this in my React form:
```
{dto.people.map((p, i) => (
<>
>
))}
```
I went down that rabbit hole for far too long, before realising ... I *can* just send a string - a **JSON** string!
## Client, version 2
This new version feels much more React-idiomatic, and does a lot less grubbing around in `form` minutae. It uses the `useFormState` hook as well as a `useState` to allow the client to snappily update while the user is doing data-entry. Then when it's submission-time, everything gets shipped off in the everything-old-is-new-again Server Actions kinda way (various validation and button-enablement niceties removed for simplicity):
```
"use client"
const initialState = {
message: ''
}
export const SelectionForm = ({ dto: DTO}) => {
const [state, formAction] = useFormState(createOrUpdateServerAction, initialState);
const [inFlightPeople, setInFlightPeople] = useState(dto.people);
const onPeopleChanged = (newPeople: Person[]) => {
setInFlightPeople(newPeople);
}
return (
)
}
```
## Server action
In case you're wondering what the server-side of this looks like:
```
"use server"
export const createOrUpdateServerAction = async (prevState: any, formData: FormData) => {
const id = parseInt(formData.get("id"), 10);
const foo = formData.get("foo");
const peopleString = formData.get("people");
const people = JSON.parse(peopleString) as Person[];
// etc
```
.. Yes, you could just `JSON.stringify()` your whole object and send that instead of using `hidden` form fields. But I kinda like seeing them make their way over the network old-skool. Maybe that's just me :-)
Labels:
frontend,
javascript,
markdown-enabled,
next.js,
react,
typescript
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment
Comments welcome - spam is not. Spam will be detected, deleted and the source IP blocked.