Requirement was simple.

Create a form with a series of fields. Few of them should have a max length validation and we had to show the user a current count v/s allowed length. Something like this:

Looks simple and one we would have been doing all this while.

However as our form was slightly large ~20 fields with validations on change firing all over the place, we found noticeable lag in the fields re rendering with updated values.

First attempt: Do an initial setup of our fields and then disconnect them from the state.

How? well, defaultValue .

<input

type="text"

name="name"

defaultValue={state.name}

onBlur={handleChange}

key={state.name}

/>

Here the fields are re rendered whenever state.name changes & it changes only on blur.

So far so good.

This however broke the character counter that as of now only updated on blur.

Not exactly useful, one might say.

So while an uncontrolled field fixed our perf issue, it broke character count.

Only way to solve it would be to somehow sync the form state, as in convert to a controlled component, but without syncing from top level form state.

We had to come up with an inner field that initialised with passed in state & then worked on it’s own state for subsequent updates.

We also had to provide a mechanism for reinitialisation as user might select a different item to edit while the fields are still rendered.

This is how our inner field roughly looks like:

If we see the effect, we can find the Inner component initialising when there is no inner state or when shouldReinitialize comes as true.

Because we are expecting the children to be a function or a render prop, we have abstracted away the behaviour and relying on the user / call site to handle rendering however they want.

This is roughly how we ended up using it:

Because now the field is controlled, we can correctly handle character counts while retaining form performance.