React Developer’s Everyday Struggle — Extending Big Applications
This article delves into the practical challenges of maintaining and scaling React applications, despite the theoretical advantages of code division into small reusable components. Callstack, in collaboration with EEDI on an educational web platform, faced the task of enhancing form validation. The complexity of making the right choice is discussed in a real-world context.
Have you ever heard about the great scalability of React applications and how easy it is to maintain the code divided into small reusable components? You probably have, and it’s true! It’s true in both theory and practice, but the latter is not that easy. And here is a real-life example showing why it’s sometimes not as simple as you might think.
Here at Callstack, we are working for EEDI building an educational web platform. In the first few months we tried to implement the most important features and we came up with a huge number of components and views. The basic functionality was built and we decided to extend the most important part of every web application — forms. We wanted to add some nice, client-side validation.
Our requirements were as follows:
- validate on each change in a field
- enable validation of all fields on submit
- specify validation rules in an easy way
- ensure validation works with all existing forms in the app
Seems easy, right? We just had to decide how to do that. Our minds were full of tweets and articles we’d read about React patterns, and it wasn’t an easy choice.
These are the most obvious solutions:
- using some third-party validation library
- create a simple <rte-code>validate<rte-code> helper — nothing fancy, just a function
- use a Higher-Order Component which would expose validation functions
Redux-form is a big but very stable validation solution for react. It’s based on Redux, and it requires you to keep your whole form state there. It gives you a simple way to “upgrade” your reducers to work with form data. You just have to combine the new reducer from the <rte-code>redux-form<rte-code>package and use the <rte-code>reduxForm<rte-code> HOC to get access to form data and errors.
Formsy-react is quite easy to use and contains a set of basic validation rules like <rte-code>isEmail<rte-code>. It allows you to extend your own <rte-code>input<rte-code> components to works with formsy. To achieve it you can use a mixin or a special HOC. It’s easy but it requires you to extend all your <rte-code>input<rte-code> components.
Formik gives you a <rte-code>withFormik<rte-code> Higher-Order Component, which you can use to wrap your form to give it access to errors data inside. It’s powerful and easy. Version <rte-code>0.9.0<rte-code> enables you also to render your form inside <rte-code><Formik /><rte-code> component but a few months ago when we were fighting with validation it wasn’t implemented yet. One of the biggest pros of this library is full support for React Native. It’s also worth to mention that it’s only 9.2 kB.
The simplest solution you can think of — just two useful functions in a separate file: <rte-code>validateField<rte-code> for validating particular fields and <rte-code>validate<rte-code>for validating the whole form. Both accept some validation rules in the following format:
<rte-code>(value: *) => boolean<rte-code>
An example usage of such a solution could look like this:
Higher Order Component
What if we create some abstraction over our helpers, as a Higher-Order Component? Creating your stuff as HOCs is (or maybe was) trendy… and useful. This component could expose a <rte-code>validate<rte-code>method and <rte-code>errors<rte-code> over the props. This is how it could be used:
On that note, let’s look at some existing form code in our EEDI application.
Here’s what it looks like:
So what do we have?
- two inputs and one button — eeeeeasy
- the requirement that the email input value should be an email and that the password is required — a harder but still easy thing we can achieve choosing one of our three solutions
- no Redux (form state inside the state of the component — this could be problematic)
- little time
- a lot of existing code we do not want to break
First of all, we would have to eliminate <rte-code>redux-form<rte-code> for an obvious reason — no Redux.
Unfortunately, that’s not everything. Two of our solutions are HOCs (the <rte-code>formik<rte-code> library and a custom HOC). Like I said earlier, we like HOCs, so this should be the best option for us. It should, but those Higher-Order Components need access to the form data to validate it. The component we want to wrap by a HOC has this data inside its state. It will be necessary, then, to create some stateful container with the data and pass the data to our form wrapped by HOC. Looks like a lot of refactoring. Refactor is always good but remember — we had “little time” and “a lot of code we did not want to break”!
The two options left were thus helpers and <rte-code>formsy-react<rte-code>.
Remember the <rte-code>Input<rte-code> component from our real-life example? It’s from our different “styleguide” repository. If it’s not necessary, we try not to touch it. That’s why we can’t use <rte-code>formsy<rte-code>, which requires changes in input components.
Got a little nervous? There’s one solution left now — a simple helper.
Looks like it meets the requirements (yeah!). But there is one problem. We are very lazy developers. We didn’t want to collect errors each time by ourselves and write a lot of duplicate code.
There should be some better option.
Function as child to the rescue
Have you ever heard about a <rte-code>function as child<rte-code>/render prop pattern? Basically, a <rte-code>Function as Child Component<rte-code> is a component that accepts a function as its child (or as some other prop) and passes some data through arguments of this function. If you want more details I’d recommend a nice talk by Michael Jackson:
Which problem does it solve? Accessing form data. It gives you similar power to a Higher-Order Component but it doesn’t require you to wrap anything and to create new components just to pass the required data into it. We can use it inside our existing form and pass form data from state as a regular prop. Just like this:
We created a simple <rte-code>Form<rte-code> component which accepts <rte-code>rules<rte-code>, <rte-code>fields<rte-code> and <rte-code>onChange<rte-code> props. <rte-code>rules<rte-code> is an object with field name as the key and a validation function as the value. fields is a similar object but as values we use the data from the form inputs. <rte-code>onChange<rte-code> is just a simple callback. All the important things like <rte-code>errors<rte-code> and the <rte-code>validate<rte-code> function are exposed to the rest of the form via the arguments of the child function.
This is our implementation:
and its usage inside a real-life example:
and a live example if you want to play with it a little:
We know this is not the best solution (especially in case of performance) but it is the best which meets our requirements and allow us to introduce validation easy and fast.
It wasn’t an easy process to find the best solution but well, it never is! The world with clear and with beautiful code, projects without changing requirements simply does not exist. The only thing we can do is continuously learn and try to use this knowledge in our projects. Don’t be afraid of using some new code patterns and not obvious solutions, just be careful.