Антипаттерны в React или вредные советы новичкам

в 8:05, , рубрики: React, ReactJS, начинающим

Привет.

Ровно год прошел с момента, как я начал изучать React. За это время я успел выпустить несколько небольших мобильных приложений, написанных на React Native, и поучаствовать в разработке web-приложения с использованием ReactJS. Подводя итог и оглядываясь назад на все те грабли, на которые я успел наступить, у меня родилась идея выразить свой опыт в виде статьи. Оговорюсь, что до начала изучения реакта, у меня имелось 3 года опыта разработки на c++, python, а также мнение, что во фронтенд разработке нет ничего сложного и разобраться во всем мне не составит труда. Поэтому в первые месяцы я пренебрегал чтением обучающей литературы и в основном просто гуглил готовые примеры кода. Соответственно, примерный разработчик, который первым делом изучает документацию, скорее всего, не найдет для себя здесь ничего нового, но я все-таки считаю, что довольно много людей при изучении новой технологии предпочитают путь от практики к теории. Так что если данная статья убережет кого-то от граблей, то я старался не зря.

Совет 1. Работа с формами

Классическая ситуация: имеется форма с несколькими полями, в которые пользователь вводит данные, после чего нажимает на кнопку, и введенные данные отправляются на внешний апи/сохраняются в state/выводятся на экран — подчеркните нужное.

Вариант 1. Как делать не надо

В React существует возможность создания ссылки на узел DOM или компонент React.

this.myRef = React.createRef();

C помощью атрибута ref созданную ссылку можно присоединить к нужному компоненту/узлу.

<input id="data" type="text" ref={this.myRef} /> 

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

class BadForm extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
    this.onClickHandler = this.onClickHandler.bind(this);
  }

  onClickHandler() {
    const data = this.myRef.current.value;
    alert(data);
  }

  render() {
    return (
      <>
        <form>
          <label htmlFor="data">Bad form:</label>
          <input id="data" type="text" ref={this.myRef} />
          <input type="button" value="OK" onClick={this.onClickHandler} />
        </form>
      </>
    );
  }
}

Как внутренняя обезьянка может попытаться оправдать данное решение:

  1. Главное, что работает, у тебя еще 100500 задач, а сериалы не смотрены тикеты не закрыты. Оставь так, потом поменяешь
  2. Смотри, как мало кода нужно для обработки формы. Объявил ref и получай доступ к данным откуда хочешь.
  3. Если будешь хранить значение в state, то при каждом изменении вводимых данных все приложение будет рендериться заново, а тебе ведь нужны только итоговые данные. Так этот метод получается еще и по оптимизации хорош, точно оставь так.

Почему обезьянка не права:

Пример выше — классический антипаттерн в React, который нарушает концепцию однонаправленного потока данных. В данном случае ваше приложение никак не сможет отреагировать на изменения данных при вводе, так как они не хранятся в state.

Вариант 2. Классическое решение

Для каждого поля формы создается переменная в state, в которой будет храниться результат ввода. Атрибуту value присваивается данная переменная. Атрибуту onСhange присваивается функция, в которой через setState() изменяется значение переменной в state. Таким образом, все данные берутся из state, а при изменении данных изменяется state и приложение рендерится заново.

class GoodForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = { data: '' };
    this.onChangeData = this.onChangeData.bind(this);
    this.onClickHandler = this.onClickHandler.bind(this);
  }

  onChangeData(event) {
    this.setState({ data: event.target.value });
  }

  onClickHandler(event) {
    const { data } = this.state;
    alert(data);
  }

  render() {
    const { data } = this.state;
    return (
      <>
        <form>
          <label htmlFor="data">Good form:</label>
          <input id="data" type="text" value={data} onChange={this.onChangeData} />
          <input type="button" value="OK" onClick={this.onClickHandler} />
        </form>
      </>
    );
  }
}

Вариант 3. Продвинутый. Когда форм становится много

У второго варианта существует ряд недостатков: большое количество стандартного кода, для каждого поля необходимо объявить метод onСhange и добавить переменную в state. Когда дело доходит до валидации введенных данных и вывода сообщений об ошибке, то количество кода возрастает еще больше. Для облегчения работы с формами существует прекрасная библиотека Formik, которая берет на себя вопросы, связанные с обслуживанием форм, а также позволяет с легкостью добавить схему валидации.

import React from 'react';
import { Formik } from 'formik';
import * as Yup from 'yup';

const SigninSchema = Yup.object().shape({
  data: Yup.string()
    .min(2, 'Too Short!')
    .max(50, 'Too Long!')
    .required('Data required'),
});

export default () => (
  <div>
    <Formik
      initialValues={{ data: '' }}
      validationSchema={SigninSchema}
      onSubmit={(values) => {
          alert(values.data);
      }}
      render={(props) => (
        <form onSubmit={props.handleSubmit}>
          <label>Formik form:</label>
          <input
            type="text"
            onChange={props.handleChange}
            onBlur={props.handleBlur}
            value={props.values.data}
            name="data"
          />
          {props.errors.data && props.touched.data ? (
            <div>{props.errors.data}</div>
          ) : null}
          <button type="submit">Ok</button>
        </form>
      )}
    />
  </div>
);

Совет 2. Избегайте мутаций

Рассмотрим простое приложение типа to-do list. В конструкторе определим в state переменную, в которой будет храниться список дел. В методе render() выведем форму, через которую будем добавлять дела в список. Теперь рассмотрим, каким образом мы можем изменить state.

Неправильный вариант, приводящий к мутации массива:

this.state.data.push(item);

В данном случае массив действительно изменился, но React об этом ничего не знает, а значит метод render() не будет вызван, и наши изменения не отобразятся. Дело в том, что в JavaScript при создании нового массива или объекта в переменной сохраняется ссылка, а не сам объект. Таким образом, добавив в массив data новый элемент, мы изменяем сам массив, но не ссылку на него, а значит значение data, сохраненное в state, не изменится.

С мутациями в JavaScript можно столкнуться на каждом шагу. Чтобы избежать мутаций данных, для массивов используйте spread оператор либо методы filter() и map(), а для объектов — spread оператор либо метод assign().

const newData = [...data, item];
const copy = Object.assign({}, obj);

Возвращаясь к нашему приложению, стоит сказать, что правильным вариантом изменения state будет использование метода setState(). Не пытайтесь менять состояние напрямую где-либо, кроме конструктора, так как это противоречит идеологии React.

Не делайте так!

this.state.data = [...data, item];

Также избегайте мутации state. Даже если вы используете setState(), мутации могут привести к багам при попытках оптимизации. Например, если вы передадите мутировавший объект через props в дочерний PureComponent, то данный компонент не сможет понять, что полученные props изменились, и не выполнит повторный рендеринг.

Не делайте так!

this.state.data.push(item);
this.setState({ data: this.state.data });

Корректный вариант:

const { data } = this.state;
const newData = [...data, item];
this.setState({ data: newData });

Совет 3. Эмуляция многостраничного приложения

Ваше приложение развивается, и в какой-то момент вы понимаете, что вам нужна многостраничность. Но как же быть, ведь React является single page application? В этот момент вам может прийти в голову следующая безумная идея. Вы решаете, что будете хранить идентификатор текущей страницы в глобальном состоянии своего приложения, например, используя redux store. Для вывода нужной страницы вы будете использовать условный рендеринг, а переходить между страницами, вызывая action с нужным payload, тем самым изменяя значения в store redux.

App.js

import React from 'react';
import { connect } from 'react-redux';
import './App.css';
import Page1 from './Page1';
import Page2 from './Page2';

const mapStateToProps = (state) => ({ page: state.page });

function AppCon(props) {
  if (props.page === 'Page1') {
    return (
      <div className="App">
        <Page1 />
      </div>
    );
  }
  return (
    <div className="App">
      <Page2 />
    </div>
  );
}
const App = connect(mapStateToProps)(AppCon);
export default App;

Page1.js

import React from 'react';
import { connect } from 'react-redux';
import { setPage } from './redux/actions';

function mapDispatchToProps(dispatch) {
  return {
    setPageHandle: (page) => dispatch(setPage(page)),
  };
}

function Page1Con(props) {
    return (
      <>
        <h3> Page 1 </h3>
        <input 
            type="button"
            value="Go to page2"
            onClick={() => props.setPageHandle('Page2')}
        />
      </>
    );
}

const Page1 = connect(null, mapDispatchToProps)(Page1Con);
export default Page1;

Чем это плохо?

  1. Данное решение — пример примитивного велосипеда. Если вы знаете, как сделать такой велосипед грамотно и понимаете на что идете, то не мне вам советовать. В противном случае, ваш код получится неявным, запутанным и излишне сложным.
  2. Вы не сможете пользоваться кнопкой назад в браузере, так как история посещений не будет сохраняться.

Как это решить?

Просто используйте react-router. Это отличный пакет, который с легкостью превратит ваше приложение в многостраничное.

Совет 4. Где расположить запросы к api

В какой-то момент вам понадобилось добавить в ваше приложение запрос к внешнему api. И тут вы задаетесь вопросом: в каком месте вашего приложения необходимо выполнить запрос?
На данный момент при монтировании React компонента, его жизненный цикл выглядит следующим образом:

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

Разберем все варианты по порядку.

В методе constructor() документация не рекомендует делать что-либо, кроме:

  • Инициализации внутреннего состояния через присвоение объекта this.state.
  • Привязки обработчиков событий к экземпляру.

Обращения к api в этот список не попадают, так что идем дальше.

Метод getDerivedStateFromProps() согласно документации существует для редких ситуаций, когда состояние зависит от изменений в props. Снова не наш случай.

Наиболее частой ошибкой является расположение кода, выполняющего запросы к api, в методе render(). Это приводит к тому, что как только ваш запрос успешно выполнится, вы, скорее всего, сохраните результат в состоянии компонента, а это приведет к новому вызову метода render(), в котором снова выполнится ваш запрос к api. Таким образом, ваш компонент попадет в бесконечный рендеринг, а это явно не то, что вам нужно.

Так что идеальным местом для обращений к внешнему api является метод componentDidMount().

Заключение

Примеры кода можно найти на github.

Автор: skoni

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js