Освоение функциональных компонентов React с помощью библиотеки Recompose

внешний интерфейс React.js
Отказ от ответственности: это не статья для ознакомления с основами React, вы должны быть знакомы со знаниями, связанными с React.

С годами я пришел к выводу, что единственный правильный способ разработки высококачественных приложений React — это написание функциональных компонентов.

В этой статье я кратко представлю функциональные компоненты и компоненты более высокого порядка. После этого мы углубимся в раздутые компоненты React и рефакторим их в элегантные решения из нескольких компонуемых компонентов более высокого порядка.

Photo byDean Pughon Unsplash

Введение в функциональные компоненты

Они называются функциональными компонентами, потому что на самом деле являются обычными функциями JavaScript. Common React должен содержать только функциональные компоненты.

Сначала давайте рассмотрим очень простой компонент класса.

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

//simple_class_component.jsx 

Теперь давайте перепишем этот же компонент как компонент-функцию:

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

//simple_functional_component.jsx

Как видите, функциональные компоненты чище, короче и легче читаются. Также нет необходимости использовать это ключевое слово.

Некоторые другие преимущества:

  • Легко рассуждать — функциональные компоненты — это чистые функции, а это значит, что они всегда будут принимать одни и те же входные данные и выводить одни и те же выходные данные. Учитывая имя Илья, вышеуказанный компонент будет отображать
<h1> Hi,Ilya </ h1>
  • Простота тестирования. Поскольку функциональный компонент является чистой функцией, его легко предсказать: задайте несколько PROP, предскажите, как он отобразит соответствующую структуру.
  • Чтобы предотвратить неправильное использование состояния компонента, используйте реквизиты.
  • Многоразовый и модульный код приветствуется.
  • Не позволяйте чрезмерно ответственным «божественным» компонентам брать на себя слишком много
  • Компонуемость — компоненты более высокого порядка могут использоваться для добавления поведения по мере необходимости.

Если у вашего компонента нет других методов, кроме метода render(), то нет причин использовать компонент класса.

компоненты более высокого порядка

Компоненты высокого порядка (HOC) — это функция повторного использования (и изоляции) логики компонентов в React. Возможно, вы сталкивались с HOC — Redux Connect является компонентом высокого порядка.

Применение HOC к компонентам расширит существующие компоненты новыми функциями. Обычно это делается путем добавления новых реквизитов, которые передаются в компонент. Для подключения Redux компонент получит новые реквизиты, которые сопоставляются с функциями mapStateToProps и mapDispatchToProps.

Нам часто нужно взаимодействовать с localStorage, однако взаимодействие с localStorage непосредственно внутри компонента является неправильным, поскольку оно имеет побочные эффекты. В обычном React компоненты не должны иметь побочных эффектов. Следующий простой компонент более высокого порядка добавит три новых реквизита в обернутый компонент и позволит ему взаимодействовать с 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

Тогда мы можем просто использовать следующий метод: withLocalStorage(MyComponent)

Беспорядочные компоненты класса

Позвольте мне представить вам компоненты, которые мы собираемся использовать. Это простая регистрационная форма, состоящая из трех полей с некоторой базовой проверкой формы.

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

Вышеупомянутый компонент запутан, он делает много вещей одновременно: обрабатывает свое состояние, проверяет поля формы и отрисовывает форму. Это уже 140 строк кода. Добавление дополнительных функций быстро становится невозможным. Можем ли мы сделать лучше?

Давайте посмотрим, что мы можем сделать.

Требуется библиотека Recompose

Recompose — это служебная библиотека React для функциональных компонентов и компонентов более высокого порядка. Думайте об этом как о lodash React.

Recompose позволяет улучшать функциональные компоненты, добавляя состояния, методы жизненного цикла, контексты и многое другое.

Самое главное, это позволяет вам иметь четкое разделение задач — у вас может быть основной компонент, предназначенный для макета, компонент более высокого порядка для обработки ввода формы, еще один для обработки проверки формы и еще один для отправки формы. Это легко проверить!

элегантные функциональные компоненты

Шаг 0. Установите Recompose

yarn add recompose

Шаг 1. Извлеките состояние формы ввода

Мы будем использовать компонент высшего порядка withStateHandlers из библиотеки Recompose. Это позволит нам изолировать состояние компонента от самого компонента. Мы будем использовать его для добавления состояний формы для полей электронной почты, пароля и подтверждения пароля, а также обработчиков событий для указанных выше полей..

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

Компонент высшего порядка withStateHandlers очень прост — он принимает начальное состояние и объект, содержащий обработчики состояния. При вызове каждый обработчик состояния возвращает новое состояние.

Шаг 2. Извлеките логику проверки формы

Теперь пришло время извлечь логику проверки формы. Мы будем использовать компонент высшего порядка withProps из Recompose. Это позволяет добавлять произвольные реквизиты к существующим компонентам.

Мы будем использовать withProps для добавления реквизитов emailError, passwordError и confirmPasswordError, которые будут выводить ошибку, если какое-либо из полей нашей формы недействительно.

Следует также отметить, что логика проверки для каждого поля формы хранится в отдельном файле (для лучшего разделения проблем).

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

Шаг 3. Извлеките логику отправки формы

На этом шаге мы извлечем логику отправки формы. Мы снова воспользуемся компонентом высшего порядка withProps, чтобы добавить обработчик onSubmit.

Функция handleSubmit принимает пропсы emailError, passwordError и confirmPasswordError, переданные с предыдущего шага, проверяет, есть ли ошибки, и если нет, запрашивает параметры у нашего API.

import { withProps } 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 = withProps(ownerProps => ({
  onSubmit: handleSubmit(ownerProps)
}));

export default withSubmitForm;

//withSubmitForm.js 

Шаг 4. Волшебство грядет

Наконец, объедините компоненты более высокого порядка, которые мы создали, в усилитель, который можно использовать в нашей форме. Мы будем использовать функцию compose из recompose, которая объединяет несколько компонентов более высокого порядка.

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

Обратите внимание на элегантность и аккуратность этого решения. Вся необходимая логика просто добавляется к другой логике для создания компонента расширения.

Шаг 5. Подышите свежим воздухом

Теперь давайте взглянем на сам компонент SignupForm.

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 

Новый рефакторинговый компонент очень чистый и делает только одну вещь — рендеринг. Принцип единой ответственности гласит, что модуль должен делать одну вещь, и делать это хорошо. Я считаю, что мы достигли этой цели.

Все необходимые данные и обработчики ввода просто передаются как свойства. Это, в свою очередь, упрощает тестирование компонентов.

Мы всегда должны стремиться к тому, чтобы наши компоненты были полностью свободными от логики и отвечали только за рендеринг. Recompose позволяет нам это сделать.

Project Source Code

Если вы столкнетесь с какими-либо проблемами в следующий раз, вы можетеgithubСкачать весь проект

Сюрприз: использование чистота, чтобы оптимизировать производительность

В Recompose есть компонент pure, который является хорошим компонентом более высокого порядка, который позволяет нам повторно отображать компоненты только тогда, когда это необходимо. pure гарантирует, что компонент не будет повторно отображаться, если какие-либо свойства не изменились.

import { compose, pure } from "recompose"; 

...

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

//pureSignupForm.js

Суммировать:

Мы всегда должны следовать принципу единой ответственности компонентов, чтобы изолировать логику от представления. Для этого, прежде всего, следует отменить метод записи компонента Class. Сам основной компонент должен быть функциональным и отвечать только за рендеринг и ни за что больше. Затем добавьте все необходимое состояние и логику в качестве компонентов более высокого порядка.

Соблюдение приведенных выше правил сделает ваш код понятным, легко читаемым, простым в сопровождении и тестировании.