Mastering React Functional Components with Recompose

Mastering React Functional Components with Recompose
Mastering React Functional Components with Recompose, his is not an introduction to React, and some familiarity with React is required.

Disclaimer: this is not an introduction to React, and some familiarity with React is required. For an introduction to React, check my other article: React.js: a better introduction to the most powerful UI library ever created.

Over the years I came to a realization that the only proper way to develop high-quality React applications is by writing stateless functional components.

In this article, I will give a brief introduction of functional components, and of higher-order components. After that, we’ll dive right in into refactoring a bloated React component into a clean and elegant solution consisting of multiple composable higher-order components.

Introduction to Functional Components

Functional Components are called so because they literally areplain JavaScript functions. An idiomatic React application should consist exclusively of functional components.

First of all let’s take a look at a very simple class component

class MyComponent extends React.Component {
  render() {
    return (
      <div>
        <h1>Hi, {this.props.name}</h1>
      </div>
    );
  }
}

simple_class_component.jsx
And now let’s rewrite the same component as a functional component:

const MyComponent = ({name}) => (
  <div>
    <h1>Hi, {name}</h1>
  </div>
);

simple_functional_component.jsx

As you can see, the functional component is much cleaner, shorter, and is easier to read. There’s also no need for the this keyword.

Some other benefits:

  • Easy to reason about - functional components are pure functions, which means that they will always produce the same output for the same input. Given name Ilya, the above component will render <h1>Hi, Ilya</h1>, no matter what.
  • Easy to test - since functional components are pure functions, it is very easy to run assertions against them: given some props, expect it to render the following markup.
  • Help prevent abuse of component state, favoring props instead.
  • Encourage reusable and modular code.
  • Discourage huge, over-complicated “god” components that have too many responsibilities.
  • Composable - behavior can be added as needed with a higher-order component.

If your component has no methods other than the render() method, then there’s really no reason to use class components.

Higher Order Components

A higher-order component (HOC) is a technique in React for reusing (and isolating) component logic. You’ve probably already encountered HOCs — Redux’s connect is a higher-order component.

Applying a HOC to a component will enhance the existing component with new features. This is usually done by adding new props that will get passed down to your component. In the case of Redux’s connect your component will get new props that got mapped with mapStateToProps and mapDispatchToProps functions.

We often need to interact with localStorage, however, interacting with localStorage directly inside of a component is wrong because it is a side effect. In idiomatic React, the components should have no side effects. The following simple higher-order component will add three new props to the wrapped component, and will enable it to interact with localStorage.

const withLocalStorage = (WrappedComponent) => {
  const loadFromStorage   = (key) => localStorage.getItem(key);
  const saveToStorage     = (key, value) => localStorage.setItem(key, value);
  const removeFromStorage = (key) => localStorage.removeItem(key);
  
  return (props) => (
      <WrappedComponent
            loadFromStorage={loadFromStorage}
            saveToStorage={saveToStorage}
            removeFromStorage={removeFromStorage}
            {...props}
        />
  );
}

simple_hoc.jsx
Then we would simply apply it in the following way: withLocalStorage(MyComponent)

A Messy Class Component

Photo by freestocks.org on Unsplash

Let me introduce you to the component we’ll be working with. It is a simple sign up form consisting of three fields with some basic form validation.

import React from "react";
import { TextField, Button, Grid } from "@material-ui/core";
import axios from 'axios';

class SignupForm extends React.Component {
  state = {
    email: "",
    emailError: "",
    password: "",
    passwordError: "",
    confirmPassword: "",
    confirmPasswordError: ""
  };

  getEmailError = email => {
    const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

    const isValidEmail = emailRegex.test(email);
    return !isValidEmail ? "Invalid email." : "";
  };

  validateEmail = () => {
    const error = this.getEmailError(this.state.email);

    this.setState({ emailError: error });
    return !error;
  };

  getPasswordError = password => {
    const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;

    const isValidPassword = passwordRegex.test(password);
    return !isValidPassword
      ? "The password must contain minimum eight characters, at least one letter and one number."
      : "";
  };

  validatePassword = () => {
    const error = this.getPasswordError(this.state.password);

    this.setState({ passwordError: error });
    return !error;
  };

  getConfirmPasswordError = (password, confirmPassword) => {
    const passwordsMatch = password === confirmPassword;

    return !passwordsMatch ? "Passwords don't match." : "";
  };

  validateConfirmPassword = () => {
    const error = this.getConfirmPasswordError(
      this.state.password,
      this.state.confirmPassword
    );

    this.setState({ confirmPasswordError: error });
    return !error;
  };

  onChangeEmail = event =>
    this.setState({
      email: event.target.value
    });

  onChangePassword = event =>
    this.setState({
      password: event.target.value
    });

  onChangeConfirmPassword = event =>
    this.setState({
      confirmPassword: event.target.value
    });

  handleSubmit = () => {
    if (
      !this.validateEmail() ||
      !this.validatePassword() ||
      !this.validateConfirmPassword()
    ) {
      return;
    }

    const data = {
      email: this.state.email,
      password: this.state.password
    };

    axios.post(`https://mywebsite.com/api/signup`, data);
  };

  render() {
    return (
      <Grid container spacing={16}>
        <Grid item xs={4}>
          <TextField
            label="Email"
            value={this.state.email}
            error={!!this.state.emailError}
            helperText={this.state.emailError}
            onChange={this.onChangeEmail}
            margin="normal"
          />

          <TextField
            label="Password"
            value={this.state.password}
            error={!!this.state.passwordError}
            helperText={this.state.passwordError}
            type="password"
            onChange={this.onChangePassword}
            margin="normal"
          />

          <TextField
            label="Confirm Password"
            value={this.state.confirmPassword}
            error={!!this.state.confirmPasswordError}
            helperText={this.state.confirmPasswordError}
            type="password"
            onChange={this.onChangeConfirmPassword}
            margin="normal"
          />

          <Button
            variant="contained"
            color="primary"
            onClick={this.handleSubmit}
            margin="normal"
          >
            Sign Up
          </Button>
        </Grid>
      </Grid>
    );
  }
}

export default SignupForm;

// complex_form.js
The above component is messy, it is doing many things at once: handling its state, validating form fields, and rendering the form. It is already at 140 lines of code. Adding any more functionality would soon make it impossible to maintain. Can’t we do any better?

Let’s see what we can do about it.

The Need for Recompose

Recompose is a React utility belt for function components and higher-order components. Think of it like lodash for React.

Recompose allows you to enhance your functional components by adding state, lifecycle methods, context, among other things.

Best of all it allows you to have clear separation of concerns - you can have the main component responsible exclusively for layout, a higher-order component responsible for handling form inputs, another one for handling form validation, another one for submitting the form. And it is very easy to test!

The Elegant Functional Component

Let’s see what can be done about the bloated class component.

Step 0. Install recompose.

Let’s add recompose to our project.

yarn add recompose

Step 1. Extract input form state.

We’ll be making use of the [withStateHandlers](https://github.com/acdlite/recompose/blob/master/docs/API.md#withstatehandlers) higher-order component from recompose. It will allow us to isolate the component state from the component itself. We’ll use it to add form state for the email, password and confirm password fields, as well as event handlers for the above-mentioned fields.

import { withStateHandlers, compose } from "recompose";

const initialState = {
  email: { value: "" },
  password: { value: "" },
  confirmPassword: { value: "" }
};

const onChangeEmail = props => event => ({
  email: {
    value: event.target.value,
    isDirty: true
  }
});

const onChangePassword = props => event => ({
  password: {
    value: event.target.value,
    isDirty: true
  }
});

const onChangeConfirmPassword = props => event => ({
  confirmPassword: {
    value: event.target.value,
    isDirty: true
  }
});

const withTextFieldState = withStateHandlers(initialState, {
  onChangeEmail,
  onChangePassword,
  onChangeConfirmPassword
});

export default withTextFieldState;

// withTextFieldState.js
The withStateHandlers higher-order component is very simple - it takes the initial state, and an object containing the state handlers. Each of the state handlers will return new state when called.

Step 2. Extract form validation logic.

Now it’s time to extract the form validation logic. We’ll be using [withProps](https://github.com/acdlite/recompose/blob/master/docs/API.md#withprops) higher-order component from recompose. It allows adding any arbitrary props to an existing component.

We’ll use withProps to add the emailError, passwordError, and confirmPasswordError props, which will contain errors if any of our form fields are invalid.

One should also note that the validation logic for every one of the form fields is kept in a separate file (for better separation of concerns).

import { withProps } from "recompose";

const getEmailError = email => {
  if (!email.isDirty) {
    return "";
  }

  const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

  const isValidEmail = emailRegex.test(email.value);
  return !isValidEmail ? "Invalid email." : "";
};

const withEmailError = withProps(ownerProps => ({
  emailError: getEmailError(ownerProps.email)
}));

export default withEmailError;

withEmailError.js

import { withProps } from "recompose";

const getPasswordError = password => {
  if (!password.isDirty) {
    return "";
  }

  const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;

  const isValidPassword = passwordRegex.test(password.value);
  return !isValidPassword
    ? "The password must contain minimum eight characters, at least one letter and one number."
    : "";
};

const withPasswordError = withProps(ownerProps => ({
  passwordError: getPasswordError(ownerProps.password)
}));

export default withPasswordError;

withPasswordError.js

import { withProps } from "recompose";

const getConfirmPasswordError = (password, confirmPassword) => {
  if (!confirmPassword.isDirty) {
      return "";
  }

  const passwordsMatch = password.value === confirmPassword.value;

  return !passwordsMatch ? "Passwords don't match." : "";
};

const withConfirmPasswordError = withProps(
    (ownerProps) => ({
        confirmPasswordError: getConfirmPasswordError(
            ownerProps.password,
            ownerProps.confirmPassword
        )
    })
);

export default withConfirmPasswordError;

withConfirmPasswordError.js

Step 3. Extract form submission logic.

In this step we’ll extract form submission logic. This time we’ll make use of the withHandlers higher-order component to add the onSubmit handler.

Why not use withProps as before? Having arrow functions in withProps significantly hurts performance — a new instance of the function will be created under the hood every time, which will result in useless re-renders. withHandlers is a special version of withProps , which is intended to be used with arrow functions.

The handleSubmit function takes the emailError , passwordError , and confirmPasswordError props passed down from the previous step, checks if there are any errors, and if not posts data to our API.

import { withHandlers } from "recompose";
import axios from "axios";

const handleSubmit = ({
  email,
  password,
  emailError,
  passwordError,
  confirmPasswordError
}) => {
  if (emailError || passwordError || confirmPasswordError) {
    return;
  }

  const data = {
    email: email.value,
    password: password.value
  };

  axios.post(`https://mywebsite.com/api/signup`, data);
};

const withSubmitForm = withHandlers({
  onSubmit: (props) => () => handleSubmit(props)
});

export default withSubmitForm;

withSubmitForm.js

Step 4. Here comes the magic.

And finally we combine the higher-order components we’ve created into a single enhancer that can be used on our form. We’ll be using compose function from recompose which enables combining multiple higher-order components.

import { compose } from "recompose";

import withTextFieldState from "./withTextFieldState";
import withEmailError from "./withEmailError";
import withPasswordError from "./withPasswordError";
import withConfirmPasswordError from "./withConfirmPasswordError";
import withSubmitForm from "./withSubmitForm";

export default compose(
    withTextFieldState,
    withEmailError,
    withPasswordError,
    withConfirmPasswordError,
    withSubmitForm
);

withFormLogic.js
Note how elegant and clean this solution is. All of the required logic is simply being added on top of another to produce one single enhancer component.

Step 5. A Breath of Fresh Air.

Now let’s take a look at the SignupForm component itself.

import React from "react";
import { TextField, Button, Grid } from "@material-ui/core";
import withFormLogic from "./logic";

const SignupForm = ({
    email, onChangeEmail, emailError,
    password, onChangePassword, passwordError,
    confirmPassword, onChangeConfirmPassword, confirmPasswordError,
    onSubmit
}) => (
  <Grid container spacing={16}>
    <Grid item xs={4}>
      <TextField
        label="Email"
        value={email.value}
        error={!!emailError}
        helperText={emailError}
        onChange={onChangeEmail}
        margin="normal"
      />

      <TextField
        label="Password"
        value={password.value}
        error={!!passwordError}
        helperText={passwordError}
        type="password"
        onChange={onChangePassword}
        margin="normal"
      />

      <TextField
        label="Confirm Password"
        value={confirmPassword.value}
        error={!!confirmPasswordError}
        helperText={confirmPasswordError}
        type="password"
        onChange={onChangeConfirmPassword}
        margin="normal"
      />

      <Button
        variant="contained"
        color="primary"
        onClick={onSubmit}
        margin="normal"
      >
        Sign Up
      </Button>
    </Grid>
  </Grid>
);

export default withFormLogic(SignupForm);

SignupForm.js
The new refactored component is very clean, and does only one thing - rendering the markup. Single responsibility principle states that a module should do one thing, and it should do it well. I believe that we have achieved this goal.

All of the required data and input handlers are simply passed down as props. This in turn makes the component extremely easy to test.

We should always strive for our components to contain no logic at all, and only be responsible for rendering. Recompose allows us to do just that.

Project Source Code

If you run into any issues following along, you can download the entire project from github.

Bonus: optimizing performance with pure

Recompose has [pure](https://github.com/acdlite/recompose/blob/master/docs/API.md#pure), which is a nice higher-order component that allows us to re-render the components only when needed. pure will ensure that the component doesn’t re-render unless any of the props have changed.

import { compose, pure } from "recompose"; 

...

export default compose(
  pure,
  withFormLogic
)(SignupForm);

pureSignupForm.js

Summary

We should always follow the single responsibility principle, and strive to isolate logic from presentation. We do this by first of all outlawing class components. The main component itself should be functional, and should only be responsible for rendering the markup and nothing else. All of the required state and logic is then added as higher-order components.

Following the above rules will make your code clear and clean, easy to read, maintainable, and easy to test.

An article on testing higher-order components is coming soon.

Recommended Courses:

React: Learn ReactJS Fundamentals for Front-End Developers

React From The Ground Up

Build Realtime Apps | React Js, Golang & RethinkDB

React for beginners tutorial

ReactJS and Flux: Learn By Building 10 Projects

Suggest:

JavaScript for React Developers | Mosh

Introduction to Functional Programming in Python

Learn React - Full Course for Beginners - React Tutorial 2019

React Tutorial - Learn React - React Crash Course [2019]

React + TypeScript : Why and How

Getting Closure on React Hooks