React Developer’s Everyday Struggle - Extending Big Applications

Authors
Jakub Kłobus
Software Engineer
@
Callstack

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

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 <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.

Simple helpers

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:

import React, { Component } from 'react';
import Input from './Input';
import { validate } from './validation';
class LoginForm extends Component {
rules = {
name: (value) => !value && 'name cannot be empty' // Some rules for each fields
}
handleSubmit = () => {
const { isValid, errors } = validate(this.rules, this.state.fields);
if (isValid) {
// TODO: submit data
} else {
this.setState({ errors });
}
}
handleFieldChange = (fieldName, value) => {
const newFields = {
...this.state.fields,
[fieldName]: value
}
const { isValid, errors } = this.props.validate(this.rules[fieldName], newFields);
this.setState(state => ({
fields: newFields,
errors
}));
}
render {
return (
<div>
<Input
label="name"
value={this.state.fields.name}
errorMessage={this.state.errors.name}
/>
<button onClick={this.handleSubmit} />
</div>
);
}
}
export default LoginForm;

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:

import React, { Component } from 'react';
import Input from './Input';
import withValidation from './withValidation';
class LoginForm extends Component {
handleSubmit = () => {
const isValid = this.props.validate();
if (isValid) {
// TODO: submit data
}
}
handleFlieldChange = (fieldName, value) => {
this.props.validateField(fieldName, value);
this.setState(state => ({
fields: {
...fields,
[fieldName]: value
}
}));
}
render {
return (
<div>
<Input
label="name"
value={this.state.fields.name}
errorMessage={this.props.errors.name}
/>
<button onClick={this.handleSubmit} />
</div>
);
}
}
export default withValidation({
name: (value) => !value && 'name cannot be empty' // Some rules for each fields
})(LoginForm);
view raw HocExample.jsx hosted with ❤ by GitHub

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.

<Form
fields={{ this.state.fields.name }}
rules={{ name: (value) => !value && 'name cannot be empty' }}
>
{(errors) => (
<Input
label="name"
value={this.state.fields.name}
errorMessage={errors.name}
/>
)}
</Form>

Here’s what it looks like:

screenshot of EEDI application form

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:

/* @flow */
import { PureComponent } from 'react';
type Fields = { [fieldName: string]: * };
type Props = {
children: Function,
rules: {
[fieldName: string]: (trimmedString: string) => void | string | boolean
},
fields: Fields,
onChange: (isValid: boolean) => void
};
type State = {
errors: Object,
isValid: ?boolean
};
class Form extends PureComponent<Props, State> {
static defaultProps: Props;
state: State = {
errors: {},
isValid: true
};
componentWillReceiveProps(nextProps: Props) {
const changedFields = Object.keys(nextProps.fields).reduce((acc, key) => {
if (nextProps.fields[key] !== this.props.fields[key]) {
return { ...acc, [key]: nextProps.fields[key] };
}
return acc;
}, {});
this.validate(changedFields);
}
validate = (fields: Fields) => {
const fieldsToValidate = fields || this.props.fields;
const errors = Object.keys(fieldsToValidate).reduce((acc, key) => {
const rule = this.props.rules[key];
if (rule) {
const originalValue = fieldsToValidate[key];
const value =
typeof originalValue === 'string'
? fieldsToValidate[key].trim()
: originalValue;
const error = rule(value) || undefined;
return { ...acc, [key]: error };
}
return acc;
}, this.state.errors);
const isValid = Object.keys(errors).filter(key => errors[key]).length === 0;
if (this.state.isValid !== isValid && this.props.onChange) {
this.props.onChange(isValid);
}
this.setState({
errors,
isValid
});
return isValid;
};
render() {
const { errors, isValid } = this.state;
return this.props.children(errors, this.validate, isValid);
}
}
export default Form;
view raw Form.jsx hosted with ❤ by GitHub

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:

/* @flow */
import { PureComponent } from 'react';
type Fields = { [fieldName: string]: * };
type Props = {
children: Function,
rules: {
[fieldName: string]: (trimmedString: string) => void | string | boolean
},
fields: Fields,
onChange: (isValid: boolean) => void
};
type State = {
errors: Object,
isValid: ?boolean
};
class Form extends PureComponent<Props, State> {
static defaultProps: Props;
state: State = {
errors: {},
isValid: true
};
componentWillReceiveProps(nextProps: Props) {
const changedFields = Object.keys(nextProps.fields).reduce((acc, key) => {
if (nextProps.fields[key] !== this.props.fields[key]) {
return { ...acc, [key]: nextProps.fields[key] };
}
return acc;
}, {});
this.validate(changedFields);
}
validate = (fields: Fields) => {
const fieldsToValidate = fields || this.props.fields;
const errors = Object.keys(fieldsToValidate).reduce((acc, key) => {
const rule = this.props.rules[key];
if (rule) {
const originalValue = fieldsToValidate[key];
const value =
typeof originalValue === 'string'
? fieldsToValidate[key].trim()
: originalValue;
const error = rule(value) || undefined;
return { ...acc, [key]: error };
}
return acc;
}, this.state.errors);
const isValid = Object.keys(errors).filter(key => errors[key]).length === 0;
if (this.state.isValid !== isValid && this.props.onChange) {
this.props.onChange(isValid);
}
this.setState({
errors,
isValid
});
return isValid;
};
render() {
const { errors, isValid } = this.state;
return this.props.children(errors, this.validate, isValid);
}
}
export default Form;
view raw Form.jsx hosted with ❤ by GitHub

and its usage inside a real-life example:

/* @flow */
import React, { Component } from 'react';
...
type State = {
passwordVisable: boolean,
email: string,
password: string,
loginPending: boolean
};
type Props = {
history: Object,
match: Object,
login: ({ email: string, password: string }) => Promise<*>,
googleLogin: ({ googleToken: string }) => Promise<*>
};
class LoginForm extends Component<Props, State> {
handleLogin = (validate: Function, e: SyntheticInputEvent<>) => {
e.preventDefault();
if (!validate()) {
return;
}
// submit data
...
};
...
render() {
const { match } = this.props;
const {
passwordVisable,
email,
password,
loginError
} = this.state;
return (
<Container>
<HeaderContainer>
<H3 fontFamily="bold">Log in to your account</H3>
<br />
<P fontFamily="medium">
You can use your Diagnostic Questions credentials :)
</P>
</HeaderContainer>
<Form
fields={{ email, password }}
rules={{
email: val => !val && 'Please enter your email address or username',
password: val => !val && 'Please enter your password'
}}
>
{(errors, validate) => (
<form onSubmit={e => this.handleLogin(validate, e)}>
<Input
error={errors.email}
underline
value={email}
onChange={e => this.handleChange('email', e)}
placeholder="Email / username"
autoFocus
/>
<Input
error={errors.password}
underline
value={password}
onChange={e => this.handleChange('password', e)}
type={passwordVisable ? 'text' : 'password'}
placeholder="Password"
iconName={passwordVisable ? 'eyeOn' : 'eyeOff'}
onIconClick={() =>
this.setState(state => ({
passwordVisable: !state.passwordVisable
}))}
/>
<FooterContainer>
<ForgotLink
onClick={() => {
this.props.history.push(`${match.url}/forgot-password`);
analyticsAction({
category: 'Click',
action: 'Forgot password'
});
}}
>
<TextS color="gray5">I forgot my password</TextS>
</ForgotLink>
<Container>
<Button
type="submit"
onClick={e => this.handleLogin(validate, e)}
loading={loginPending}
>
Log in
</Button>
</Container>
</FooterContainer>
</form>
)}
</Form>
</Container>
);
}
}
...
export default LoginForm;

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 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.

Insights

Learn more about Scalability

cover

Jul 16Podcast

Structure Monorepo for Speed, Scale & Sanity

Discover how Nx empowers teams to build and scale monorepos without losing their sanity, from selective testing to AI-powered workflows.

Event Cover

Sep 7Talk

The Invisible Architect

Learn how communication shapes your app development, from code to design. Discover React’s role and leverage Conway’s Law for success.

cover

Dec 27Article

Why Is It Important to Always Run The Latest Version of React Native?

Upgrade your app to the latest version to get more features and better support. Find out more in our article.

cover

Aug 13Article

12 Rules and Approaches of Code Writing for Beginners

If you are a beginning programmer, we prepared a list of 12 rules and approaches of code writing that will help you in your coding journey!

cover

May 28Article

Building Component Libraries for React Native Apps

Satyajit Sahoo shares insights on building component libraries for React Native apps.

cover

Mar 1Article

Reusable React Native Components

It’s pretty hard to find simple methods to customize components that could be reused within the app. This article will help you create more efficient code.

cover

Feb 8Article

Four Tips to Write Maintainable React Components

React - the declarative UI framework that simplifies development. Read our articles for more details.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.