Привет, Хабр.
Ровно год прошел с момента, как я начал изучать React. За это время я успел выпустить несколько небольших мобильных приложений, написанных на React Native, и поучаствовать в разработке web-приложения с использованием ReactJS. Подводя итог и оглядываясь назад на все те грабли, на которые я успел наступить, у меня родилась идея выразить свой опыт в виде статьи. Оговорюсь, что до начала изучения реакта, у меня имелось 3 года опыта разработки на c++, python, а также мнение, что во фронтенд разработке нет ничего сложного и разобраться во всем мне не составит труда. Поэтому в первые месяцы я пренебрегал чтением обучающей литературы и в основном просто гуглил готовые примеры кода. Соответственно, примерный разработчик, который первым делом изучает документацию, скорее всего, не найдет для себя здесь ничего нового, но я все-таки считаю, что довольно много людей при изучении новой технологии предпочитают путь от практики к теории. Так что если данная статья убережет кого-то от граблей, то я старался не зря.
Классическая ситуация: имеется форма с несколькими полями, в которые пользователь вводит данные, после чего нажимает на кнопку, и введенные данные отправляются на внешний апи/сохраняются в state/выводятся на экран — подчеркните нужное.
В React существует возможность создания ссылки на узел DOM или компонент React.
C помощью атрибута ref созданную ссылку можно присоединить к нужному компоненту/узлу.
Таким образом, задачу выше можно решить, создав ref для каждого поля формы, а в теле функции, вызываемой при нажатии на кнопку, получить данные из формы, обратившись к нужным ссылкам.
Как внутренняя обезьянка может попытаться оправдать данное решение:
Почему обезьянка не права:
Пример выше — классический антипаттерн в React, который нарушает концепцию однонаправленного потока данных. В данном случае ваше приложение никак не сможет отреагировать на изменения данных при вводе, так как они не хранятся в state.
Для каждого поля формы создается переменная в state, в которой будет храниться результат ввода. Атрибуту value присваивается данная переменная. Атрибуту onСhange присваивается функция, в которой через setState() изменяется значение переменной в state. Таким образом, все данные берутся из state, а при изменении данных изменяется state и приложение рендерится заново.
У второго варианта существует ряд недостатков: большое количество стандартного кода, для каждого поля необходимо объявить метод onСhange и добавить переменную в state. Когда дело доходит до валидации введенных данных и вывода сообщений об ошибке, то количество кода возрастает еще больше. Для облегчения работы с формами существует прекрасная библиотека Formik, которая берет на себя вопросы, связанные с обслуживанием форм, а также позволяет с легкостью добавить схему валидации.
Рассмотрим простое приложение типа to-do list. В конструкторе определим в state переменную, в которой будет храниться список дел. В методе render() выведем форму, через которую будем добавлять дела в список. Теперь рассмотрим, каким образом мы можем изменить state.
Неправильный вариант, приводящий к мутации массива:
В данном случае массив действительно изменился, но React об этом ничего не знает, а значит метод render() не будет вызван, и наши изменения не отобразятся. Дело в том, что в JavaScript при создании нового массива или объекта в переменной сохраняется ссылка, а не сам объект. Таким образом, добавив в массив data новый элемент, мы изменяем сам массив, но не ссылку на него, а значит значение data, сохраненное в state, не изменится.
С мутациями в JavaScript можно столкнуться на каждом шагу. Чтобы избежать мутаций данных, для массивов используйте spread оператор либо методы filter() и map(), а для объектов — spread оператор либо метод assign().
Возвращаясь к нашему приложению, стоит сказать, что правильным вариантом изменения state будет использование метода setState(). Не пытайтесь менять состояние напрямую где-либо, кроме конструктора, так как это противоречит идеологии React.
Не делайте так!
Также избегайте мутации state. Даже если вы используете setState(), мутации могут привести к багам при попытках оптимизации. Например, если вы передадите мутировавший объект через props в дочерний PureComponent, то данный компонент не сможет понять, что полученные props изменились, и не выполнит повторный рендеринг.
Не делайте так!
Корректный вариант:
Но даже вариант выше может привести к трудноуловимым багам. Дело в том, что никто не гарантирует, что за время прошедшее между получением переменной data из state и записью ее нового значения в state, само состояние не изменится. Таким образом, вы рискуете потерять часть сделанных изменений. Поэтому в случае, когда вам необходимо обновить значение переменной в state, используя ее предыдущие значение, делайте это следующим образом:
Корректный вариант, если следующее состояние зависит от текущего:
Ваше приложение развивается, и в какой-то момент вы понимаете, что вам нужна многостраничность. Но как же быть, ведь React является single page application? В этот момент вам может прийти в голову следующая безумная идея. Вы решаете, что будете хранить идентификатор текущей страницы в глобальном состоянии своего приложения, например, используя redux store. Для вывода нужной страницы вы будете использовать условный рендеринг, а переходить между страницами, вызывая action с нужным payload, тем самым изменяя значения в store redux.
App.js
Page1.js
Чем это плохо?
Как это решить?
Просто используйте react-router. Это отличный пакет, который с легкостью превратит ваше приложение в многостраничное.
В какой-то момент вам понадобилось добавить в ваше приложение запрос к внешнему api. И тут вы задаетесь вопросом: в каком месте вашего приложения необходимо выполнить запрос?
На данный момент при монтировании React компонента, его жизненный цикл выглядит следующим образом:
Разберем все варианты по порядку.
В методе constructor() документация не рекомендует делать что-либо, кроме:
Обращения к api в этот список не попадают, так что идем дальше.
Метод getDerivedStateFromProps() согласно документации существует для редких ситуаций, когда состояние зависит от изменений в props. Снова не наш случай.
Наиболее частой ошибкой является расположение кода, выполняющего запросы к api, в методе render(). Это приводит к тому, что как только ваш запрос успешно выполнится, вы, скорее всего, сохраните результат в состоянии компонента, а это приведет к новому вызову метода render(), в котором снова выполнится ваш запрос к api. Таким образом, ваш компонент попадет в бесконечный рендеринг, а это явно не то, что вам нужно.
Так что идеальным местом для обращений к внешнему api является метод componentDidMount().
Примеры кода можно найти на github.
Ровно год прошел с момента, как я начал изучать 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>
</>
);
}
}
Как внутренняя обезьянка может попытаться оправдать данное решение:
- Главное, что работает, у тебя еще 100500 задач, а
сериалы не смотренытикеты не закрыты. Оставь так, потом поменяешь - Смотри, как мало кода нужно для обработки формы. Объявил ref и получай доступ к данным откуда хочешь.
- Если будешь хранить значение в 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 });
Но даже вариант выше может привести к трудноуловимым багам. Дело в том, что никто не гарантирует, что за время прошедшее между получением переменной data из state и записью ее нового значения в state, само состояние не изменится. Таким образом, вы рискуете потерять часть сделанных изменений. Поэтому в случае, когда вам необходимо обновить значение переменной в state, используя ее предыдущие значение, делайте это следующим образом:
Корректный вариант, если следующее состояние зависит от текущего:
this.setState((state) => {
return {data: [...state.data, item]};
});
Совет 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;
Чем это плохо?
- Данное решение — пример примитивного велосипеда. Если вы знаете, как сделать такой велосипед грамотно и понимаете на что идете, то не мне вам советовать. В противном случае, ваш код получится неявным, запутанным и излишне сложным.
- Вы не сможете пользоваться кнопкой назад в браузере, так как история посещений не будет сохраняться.
Как это решить?
Просто используйте react-router. Это отличный пакет, который с легкостью превратит ваше приложение в многостраничное.
Совет 4. Где расположить запросы к api
В какой-то момент вам понадобилось добавить в ваше приложение запрос к внешнему api. И тут вы задаетесь вопросом: в каком месте вашего приложения необходимо выполнить запрос?
На данный момент при монтировании React компонента, его жизненный цикл выглядит следующим образом:
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
Разберем все варианты по порядку.
В методе constructor() документация не рекомендует делать что-либо, кроме:
- Инициализации внутреннего состояния через присвоение объекта this.state.
- Привязки обработчиков событий к экземпляру.
Обращения к api в этот список не попадают, так что идем дальше.
Метод getDerivedStateFromProps() согласно документации существует для редких ситуаций, когда состояние зависит от изменений в props. Снова не наш случай.
Наиболее частой ошибкой является расположение кода, выполняющего запросы к api, в методе render(). Это приводит к тому, что как только ваш запрос успешно выполнится, вы, скорее всего, сохраните результат в состоянии компонента, а это приведет к новому вызову метода render(), в котором снова выполнится ваш запрос к api. Таким образом, ваш компонент попадет в бесконечный рендеринг, а это явно не то, что вам нужно.
Так что идеальным местом для обращений к внешнему api является метод componentDidMount().
Заключение
Примеры кода можно найти на github.