[Из песочницы] Антипаттерны в React или вредные советы новичкам

?v=1

Привет, Хабр.

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

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


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

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


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

this.myRef = React.createRef();


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

 


Таким образом, задачу выше можно решить, создав 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 (
      <>
        
); } }


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

  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 (
      <>
        
); } }


Вариант 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 () => (
  
{ alert(values.data); }} render={(props) => (
{props.errors.data && props.touched.data ? (
{props.errors.data}
) : null}
)} />
);


Совет 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 (
      
); } return (
); } 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 (
      <>
        

Page 1

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.

© Habrahabr.ru