Привет, Хабр.
Ровно год прошел с момента, как я начал изучать 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.