@radekmie

On uniforms v4

By Radosław Miernik · Published on

Table of contents

Intro

Have you heard of uniforms? It’s a schema-based React forms library that I’ve been working on at Vazco for more than 6 years now. We released 177 versions and achieved more than 1.6 million downloads in that time. It may not be a lot for you, but it really means a lot to me.

Back in January 2021, we released v3. It included migration to the new React Context API, a handful of TypeScript improvements, simplification of the validation flow, and… A lot of removals. Really, check out the migration guide yourself – there are around 15 removed methods or props.

Last week, we had a meeting regarding version 4. And you know what? We are on a killing removal spree again. Everything to make both our and the users’ lives easier. Or at least that’s the goal.

Handling default values

Most of the schemas out there have a concept of default values. The goal is to fill missing values with some actual values to ensure that the value exists at all times. Also, it implies that there’s some sort of mutation happening, i.e., the schema doesn’t only verify but also adjusts some fields. (That’s often referred to as the “cleaning” process; it also handles type coercion.)

In uniforms, the form component itself doesn’t calculate the initial model (form values) on its own but delegates it to the field components instead. It’s handled during the first render, and requires additional logic in the form component. For now, let’s assume that it’s fine as it is.

One of the reports says that it doesn’t work for optional fields. That’s true, and it was built like that for a reason – my assumption was that because the default value will be added later in the cleaning process, we don’t have to set them explicitly in the first place.

It’s a reasonable approach, but it breaks as soon as any piece of the code would like to use the field’s value before cleaning. That’s not unusual in practice, as both conditional rendering and dependent validation do that. (Also, cleaning is often costly and may require a traversal of the entire model.)

There’s one more thing to it. As the default values are set (using onChange) when the field is being rendered, it may result in multiple rerenders when a field appears in an already rendered form.

What can we do about it? We’ve discussed a few options but finally settled on a complete overhaul of the process. We want to eliminate all of the potential rerenders, make it work for both required and optional fields, and actually be more schema-centric. (Now it’s somewhere between the schema and the UI.)

We plan to introduce a new getInitialModel method on a bridge instance, which will be called when the form is rendered without any initial values. Luckily, we think it won’t require any changes in the custom bridges, as we can actually implement it using the existing getInitialValue (that gets simplified too) and getSubfields methods.

As a result, rendering becomes simpler and effect-free, none of the form components have to differentiate between different onChange calls, and the default values handling will now be easily explainable.

Handling enums

As a schema-based form library, uniforms must know what kind of data the schema defines. Getting a list of fields and their types is one thing, but knowing how to render them (i.e., which components to use) requires some heuristics. (Luckily, we figured it out a long time ago.)

Now, one of these heuristics is this – if there’s an allowedValues prop, we’ll render a dropdown or a set of checkboxes (or radio buttons, depending on the case). That’s simple enough but relies on one condition – the bridge (a schema wrapper) has to know whether a field should be given the said prop.

You may think that it’s a weird name for a prop, and you’re totally right. The first version of uniforms was based on the aldeed:simple-schema Meteor package, and that’s where the name comes from.

Now, in most cases, the value you store in your database is different from the ones that you’d like your users to pick from. Sometimes it’s as easy as making Repeat weekly out of repeat_weekly, but other times sometimes it requires a translation of an ID into a user-friendly text (e.g., username).

How to do it in uniforms? All components that accept the allowedValues prop accept transform as well. The idea is simple: it makes the label out of a value. Implementation is trivial, and does what it’s supposed to; everyone’s happy.

A couple of years later, a feature request appeared – someone would like to disable some of the options. It totally makes sense to do so, but it requires yet another prop. That’s how the disableItem got added. We were not that happy with it, but it solved a problem at a relatively low cost.

But then it happened again – this time someone would like to customize the key prop of the values. Again, it makes sense, but it requires another prop. We could add it, but… It has to stop somewhere. What if we’d like to add even more options there? We can’t add more and more props endlessly.

In the meantime, on the bridge side, we added support for options – a handy shorthand for allowedValues and transform in one array or object. It makes sense and is really useful, but it increases the complexity even further too. And as it was resolved by the bridge, not the components, it became impossible to type. (It’s because the field components are unaware of the bridge’s capabilities.)

So, what can we do about all of this? Firstly, it has to be one way of doing it across all of the bridges and themes. Secondly, it has to cover all of the current use cases as well as the new key one. And lastly, it must be possible to type it properly. (Bonus points for making it extensible per theme.)

Our solution is to get rid of all of the aforementioned props and replace them with one, called options, which will be an array of values and their metadata. It defines a set of basic properties that all packages have to understand, but if some of them want to be capable of doing more – they’re allowed to do so.

It sounds like a huge change, but is it really that severe? Sure, a few props are being changed, and part of the enum-handling logic in the bridges has to be adjusted accordingly. But as a result, the API surface is smaller, there are no exceptions, and everything is type-safe.

Closing thoughts

These are only two examples of what kind of changes you can expect from v4 of uniforms, but I guess it kind of gives out the vibe we’re in. Adding new APIs is great and often provides immediate value, but making the development more streamlined and eliminating common foot guns is something that makes people keener to stick with the package.

Not to make this whole text about removing things, let me mention two issues I created this weekend: #1159 and #1160. The former is about adding a new, zod-based bridge, as it gets more and more popular, and we think it may be enough for some apps.

The latter is an idea of adding a new Form component that’d cover all of the *Form components we have now, but in a more organized fashion. And yes, we want to remove the others in v5.

Are you surprised? I said we’re on a removal spree!