Schlez
2023

Easy and Performant Partial Form Submissions

Choosing which fields to send in a form, without lifting state in a React app.Published .

Israel is at war, and I was called up for reserve duty to help build some critical applications to manage the warfare. We started using Next.js with its latest and greatest features, which enabled us to ship so quickly. I'm telling you, RSC and Server Actions are incredibly efficient

🤌tl;dr

You can use the form attribute on <input /> tags to implement a partial form submission without "lifting" a state and keep the state local to your input component. Check out the sandbox for the implementation or read on to understand what is going on with all of these fancy words.

As with most enterprise web apps, we faced challenges that we had to overcome. One of them was interesting enough to write about.

My good friend Dean implemented an Excel-like interface to assist power users in filling out details in the quickest way possible. However, we only wanted to publish a subset of the rows each time. Our initial approach was to use controlled components everywhere.

👀ohhh btw

A controlled component is a <input /> component that has both value and onChange props, making React control this component state, rather than reactively reading it only when necessary

The "React" solution of holding a state of multiple components is to lift the state. This means that every time we add or remove a row from the form. Our data set has around 50 columns and 5000 rows. Therefore, every re-rendering happening at the root would cause a massive re-render cycle that could freeze the main thread for an unacceptable amount of time.

So we thought, how can we make this state change local to the leaf component? or in other React terms: how can we avoid lifting the state?

Turns out, it is completely possible to keep the state local to the row component. We figured out a great hack that solved our performance issues altogether.

We began by creating an independent form element:

<>
  <form action={myServerAction} id="partially-sent-form" />
  <Rows />
</>

When implementing each Row, it will now need to:

  1. contain a local isChecked state
  2. if isChecked is true, then all form elements will have a form attribute that references our form: form="partially-sent-form" attribute, which will attach them to the standalone form

Or, in a simplified example:

function Row(props: RowProps) {
  const [isChecked, setIsChecked] = useState(false);
  return (
    <div>
      <input type="check" onChange={() => isChecked((v) => !v)} />
      <input
        type="text"
        form={isChecked ? "partially-sent-form" : undefined}
        placeholder="text here..."
        name={`${props.id}.field1`}
      />
    </div>
  );
}

What benefits does this approach provide us?

  1. State is local to the Row component: there are no extra re-renders and it's extremely fast.
  2. In order to get all the selected fields we can use the FormData constructor. We're effectively using the platform. 😉
  3. As it uses a form and server actions, we can take advantage of all the React hooks and the conveniences provided by Next.js.
👀ohhh btw

While this approach requires field name manipulation on the server side, it's not a hard task and, in my opinion, is worth the reduction in complexity and performance improvements. We decided that the field names will always follow the format ${rowId}.${fieldName}, which is relatively easy to parse and decode.

It's astounding how quickly a standard web app necessitates technical solutions for seemingly simple problems. I was incredibly excited when we came up with this idea to solve this particular issue. I'm continually amazed at how much I can learn from tackling even the most mundane tasks.

Always learning. 😌

... ah right. A sandbox! Check this out:

Read more