React Developer’s Everyday Struggle — Extending Big Applications
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 in 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 validate
helper — nothing fancy, just a function
- use a Higher-Order Component which would expose validation functions
Third-party library
The most popular React validation libraries are redux-form, formsy-react and formik.
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 redux-form
package and use thereduxForm
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 isEmail
. It allows you to extend your own input
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 input
components.
Formik gives you a withFormik
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 0.9.0
enables you also to render your form inside <Formik />
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.
Simple helpers
The simplest solution you can think of — just two useful functions in a separate file: validateField
for validating particular fields and validate
for validating the whole form. Both accept some validation rules in the following format:(value: *) => boolean
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 validate
method and errors
over the props. This is how it could be used:
Real life
Okay, so we had a few options to choose from. We also knew, however, that they were only ideas and our task was to put them into practice. And remember - the real world (especially in places with JavaScript as an official language) can be dark and cruel.
On that note, let’s look at some existing form code in our EEDI application.
Here’s how 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 redux-form
for an obvious reason — no Redux.
Unfortunately, that’s not everything. Two of our solutions are HOCs (the formik
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 formsy-react
.
Remember the Input
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 formsy
, 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 afunction as child
/render prop
pattern? Basically, a Function as Child Component
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 — https://www.youtube.com/watch?v=BcVAq3YFiuc
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 Form
component which accepts rules
, fields
and onChange
props. rules
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. onChange
is just a simple callback. All the important things like errors
and the validate
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:
Problem solved!
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 such is life. 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!