Главная » Статьи » Как не надо писать React: неправильные шаблоны и проблемы в React

Как не надо писать React: неправильные шаблоны и проблемы в React

Как не надо писать React: неправильные шаблоны и проблемы в React

От автора: что такое антишаблон? Антишаблоны – это определенные шаблоны в разработке ПО, считающиеся плохими практиками программирования. Некоторые React шаблоны могли считаться правильными в прошлом, но сейчас разработчики поняли, что они приносят больше проблем и багов в долгосрочной перспективе. React стал полноценной UI библиотекой, и за эти годы развилось множество лучших практик разработки. Мы извлечем уроки из коллективной мудрости тысяч программистов и разработчиков, которые изучили их.

1. Bind() и стрелочные функции в компонентах

Прежде чем использовать пользовательские функции как свойства для компонентов их необходимо обернуть в функцию constructor. Если объявлять компоненты через ключевое слово extends, пользовательские функции (типа extends ниже) теряют свои привязки к this. Если необходим доступ к this.state, или this.props или this.setState, то придется перепривязать их. Демо

class app extends Component { constructor(props) { super(props); this.state = { name: '' }; this.updateValue = this.updateValue.bind(this); }
updateValue(evt) { this.setState({ name: evt.target.value }); }
render() { return ( <form> <input onChange={this.updateValue} value={this.state.name} /> </form> ) }
}

Проблемы

Есть два способа привязать пользовательские функции к this компонента. Первый – привязать в constructor, как сделано выше. Второй – привязать в момент передачи как значение свойства –

<input onChange={this.updateValue.bind(this)} value={this.state.name} />

У этого метода есть проблема. .bind() создает новую функцию при каждом запуске, поэтому этот метод будет создавать новую функцию каждый раз при выполнении функции render. Это бьет по производительности. В маленьких приложениях это будет незаметно. Разница начнет проявляться по мере роста приложения. Пример.

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

<input onChange={ (evt) => this.setState({ name: evt.target.value }) } value={this.state.name} />

Такой способ записи намного понятнее. Здесь видно, что происходит в самом свойстве onChange. Но этот метод создает новую анонимную функцию при каждом рендере input. Поэтому и возникают те же проблемы с производительностью, что и выше.

Решения

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

Часто мы забываем bind свои функции в constructor, после чего получаем ошибку (Cannot find X on undefined). В Babel есть плагин, который позволяет писать auto-bound функции на синтаксисе жирных стрелок. Плагин — Class properties transform. Теперь компоненты можно писать следующим образом –

class App extends Component { constructor(props) { super(props); this.state = { name: '' };
// Look ma! No functions to bind!
}
updateValue = (evt) => { this.setState({ name: evt.target.value }); }
render() { return ( <form> <input onChange={this.updateValue} value={this.state.name} /> </form> ) }
}

2. Использование индексов в свойстве key

Key – основное свойство при прогоне коллекции элементов. Ключи должны быть стабильными, предсказуемыми и уникальными, чтобы React мог отслеживать элементы. React использует ключи, чтобы легко согласовывать (read: update) разницу между виртуальным DOM и реальным DOM. Однако использование определенного набора значений типа индексов массива может поломать приложение или привести к рендеру неверных данных. Демо

{elements.map((element, index) => <Display {...element} key={index} /> )
}

Проблемы

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

Как не надо писать React: неправильные шаблоны и проблемы в React

По ссылке можно найти список проблем, вызванных использованием индексов в качестве keys.

Решения

Используемые вами ключи должны быть –

Уникальны – key элемента должен быть уникален среди элементов одного уровня. Глобальная уникальность необязательна.

Стабильными – key элемента не должен меняться со временем или после обновления страницы, или после перестановки элементов.

Предсказуемым – всегда должна быть возможность получить тот же key при необходимости. То есть key не должен генерироваться случайно.

Индексы массива уникальны и предсказуемы. Но они не стабильны. Точно так же нельзя использовать в качестве key случайные числа и временные метки.

Использовать случайные числа значит не использовать key совсем, так как случайные числа не уникальны и не стабильны. Компонент будет постоянно повторно отрисовываться, даже если контент внутри элемента не менялся.

Временные метки уникальны, но не стабильны и не предсказуемы. Они постоянно увеличиваются. То есть при обновлении страницы мы будем получать всегда новые временные метки.

В общем, необходимо полагаться на ID, сгенерированный базой данных. Например, первичным ключом в реляционных базах данных и Object ID в Mongo. Если ID базы данных не доступен, можно сгенерировать хэш контента и использовать его в качестве key.

3. setState() – асинхронен

Компоненты React состоят из 3 вещей: state ,props и разметки (или других компонентов). Props менять нельзя. Но можно менять state. Изменение state вызывает повторный рендер компонента. Если state управляется внутренне через компонент, то за обновление состояния отвечает функция this.setState. Об этой функции нужно знать пару фактов. Давайте разбираться – Демо

class MyComponent extends Component { constructor(props) { super(props); this.state = { counter: 350 }; } updateCounter() { // this line will not work this.state.counter = this.state.counter + this.props.increment; // --------------------------------- // this will not work as intended this.setState({ counter: this.state.counter + this.props.increment; // May not render }); this.setState({ counter: this.state.counter + this.props.increment; // what value this.state.counter have? }); // --------------------------------- // this will work this.setState((prevState, props) => ({ counter: prevState.counter + props.increment })); this.setState((prevState, props) => ({ counter: prevState.counter + props.increment })); }
} 

Проблемы

Взгляните на строку 11. Если напрямую мутировать state, то компонент не будет повторно отрисован, и вы не увидите изменения. Это происходит потому, что state сравнивается неглубоко. Всегда используйте setState для изменения значения state.

Если в setState использовать значение текущего state для обновления следующего state (как в строке 15), React может выполнить повторный рендер, а может и не выполнить. Это происходит потому, что state и props обновляются асинхронно. То есть DOM не обновляется при вызове setState. Вместо этого React складывает несколько обновлений в одно и затем отрисовывает DOM. Если запросить объект state, можно получить устаревшие значения. Об этом говорится в документации

«Не нужно полагаться на значения this.props и this.state для вычисления следующего состояния, так как они могут обновляться асинхронно.»

Другая проблема – несколько вызовов setState в одной функции (строки 16 и 20, как показано выше). Начальное значение счетчика равно 350. Предположим, что значение this.props.increment равно 10. Вы можете подумать, что после первого выполнения setState в строке 16 значение счетчика изменится на 350+10 = 360. А при втором вызове setState в строке 20 значение счетчика будет 360+10 = 370. Но это не так. Второй вызов все еще видит значение counter 350. Это происходит потому, что setState асинхронен. Значение счетчика не изменится до следующего цикла обновления. Выполнение setState ожидает в цикле событий. Пока не закончится выполнение updateCounter, setState не будет запущен, а значит, state не обновится.

Решение

Используйте другую форму setState, как в строке 27 и 31. В этой форме можно передавать функцию в setState, которая получает аргументы currentState и currentProps. Возвращаемое значение этой функции сливается с существующим state и формирует новое state.

4. Свойства в начальном состоянии

В документации React упоминается антишаблон –

«Использование свойств для генерации state в getInitialState часто приводит к дублированию «source of truth», т.е. возникает проблема, как отличить реальные данные. Так происходит потому, что getInitialState выполняется только при первом создании компонента.» Демо

import React, { Component } from 'react' class MyComponent extends Component { constructor(props){ super(props); this.state = { someValue: props.someValue, }; }
} 

Проблемы

constructor или getInitialState вызывается только в момент создания компонента. То есть constructor выполняется один раз. Поэтому при следующем изменении props state не обновится и сохранит предыдущее значение.

Молодые разработчики часто думают, что значения props синхронизируются со state, и при изменении props state будет обновлять эти значения. Но это не так.

Решения

Этот шаблон можно использовать для определенного поведения. Т.е. вы хотите, чтобы state получил значения props всего один раз. State будет управляться внутренне компонентом.

В других случаях можно использовать метод жизненного цикла componentWillReceiveProps для синхронизации state и props, как показано ниже.

import React, { Component } from 'react' class MyComponent extends Component { constructor(props){ super(props); this.state = { someValue: props.someValue, }; } componentWillReceiveProps(nextProps){ if (nextProps.inputValue !== this.props.inputValue) { this.setState({ inputVal: nextProps.inputValue }) } } } 

Обратите внимание, что у componentWillReceiveProps есть свои недостатки. Можете прочесть об этом в документации. Лучше всего использовать библиотеку управления состоянием типа Redux для подключения state и компонента.

5. Имя компонента

Если рендерить компонент в React через JSX, то имя компонента должно начинаться с заглавной буквы. Демо

<MyComponent> <app /> // Will not work :( </MyComponent>
<MyComponent> <App /> // Will work!
</MyComponent>

Проблемы

Если создать компонент app и отрендерить его через JSX в виде <app label=»Save» />, React выбросит ошибку.

Как не надо писать React: неправильные шаблоны и проблемы в React

В ошибке сказано, что не распознан. С нижнего регистра могут начинаться только элементы HTML и SVG. Поэтому <div /> пройдет, а <app> — нет.

Решение

Необходимо убедиться, что при использовании пользовательского компонента в JSX, он должен начинаться с заглавной буквы. Объявление компонентов не должно подчиняться этому правилу. Поэтому можно сделать так –

// Here lowercase is fine.
class primaryButton extends Component { render() { return <div />; }
}
export default primaryButton;
// In a different file, import the button. However, make sure to give a name starting with capital letter.
import PrimaryButton from 'primaryButton';
<PrimaryButton />

Это были некоторые неинтуитивные и трудно понятные моменты, которые приводят к появлению багов в React. Оставляйте в комментариях другие антишаблоны, которые вы знаете.

Автор: Arfat Salman

Источник: https://codeburst.io/

Редакция: Команда webformyself.