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 (
// render the people, wire in the onPeopleChanged handler