Функциональный setState() — это будущее React.

Перевод c английского статьи Justice Mba

React способствовал популяризации функционального программирования в мире Javascript. Это привело к тому, что в мире фреймворков Javascript все больше стали появляться паттерны, основанные на компонентно-ориентированной архитектуре UI, которая используется в React. И теперь всеобщая одержимость функциональным программированием все больше и больше проявляется в мире веб-разработки.

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

Итак, сегодня я открою вам новую фишку в React, до этого хранящуюся в полном секрете — функциональный setState!

Окей. Ладно, я только что сам придумал название этому приему, ну и… вообще вряд ли это что-то кардинально новое или там какой-нибудь секрет. Не совсем. Вообще, это прием программирования в React, который знаком только немногим девелоперам, которые реально познали все глубины и механику React. Поэтому ранее оно не имело своего названия. Зато теперь имеет — функциональный setState!

Описывая что это такое вкратце, можно сказать что:

Функциональный setState — это изменение состояния компонента отдельно от объявления его класса.

Нижеследующее вам наверняка известно.

React — это компонентно-ориентированная UI библиотека. В основе каждого компонента лежит функция, которая принимает некоторые свойства и возвращает UI элемент.

function User(props) {
return (
<div>A pretty user</div>
);
}

Компонент может иметь состояние, и в этом случае должен уметь управлять им. В этом случае, компонент обычно объявляется как класс, а его состояние объявляется в его функции-конструкторе.

class User {
constructor () {
this.state = {
score : 0
};

}
render () {
return (
<div>This user scored {this.state.score}</div>
);
}
}

Чтобы управлять состоянием, React предоставляет специальный метод setState().

Используется он примерно так:

class User {
...
increaseScore () {
this.setState({score : this.state.score + 1});
}
...
}

Обратите внимание как работает setState(). Вы передаете ему объект, содержащий часть информации, которую вы хотите обновить. Другими словами, значения в передаваемом объекте будут сначала сравниваться со значениями из состояния компонента и, после, setState() либо обновит, либо добавит новые значения в state.

А вот это уже вам может быть неведомо.

Я думаю теперь вы поняли как работает setState(), но… Что если я вам скажу что вместо того, чтобы передавать объект — вы можете передавать функцию?

Все верно. setState() также принимает функцию. Функция в своих аргументах принимает текущий state, а также props(свойства) компонента, которые используются для вычислений следующего состояния.

this.setState(function(state, props) {
return {
score: state.score - 1
}
});

Заметьте, что setState()— функция, а в качестве аргументов мы передаем ей другую функцию. Вот что я называю функциональным программированием.

На первый взгляд, выглядит не слишком привлекательно, приходится писать больше кода — но для чего? Только лишь для того чтобы изменить state?

Зачем нужен функциональный setState()?

Сразу сорвем покровы и скажем: для того чтобы можно было обновлять состояние асинхронно!

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

Как видите, не все так просто. Хотя, по правде говоря, это сильно упрощенное описание. Поскольку внутренняя механика setState() достаточно сложна, его вызов вполне может обновлять состояние не сразу.

React, оптимизируя свою работу, может на несколько вызовов setState( ) — лишь один раз обновить состояние приложения.

Что это значит?

Во-первых, множественные вызовы setState() могут означать несколько изменений в рамках одной функции, как здесь:

state = {score : 0};
// multiple setState() calls
increaseScoreBy3 () {
this.setState({score : this.state.score + 1});
this.setState({score : this.state.score + 1});
this.setState({score : this.state.score + 1});
}

Вместо того, чтобы 3 раза выполнять обновление состояния, React избегает такого огромного количества работы, производит необходимые вычисления до начала работы со состоянием и затем только обновляет.

Запомните: то, что вы передаете в setState() — обычный объект. Каждый раз когда React сталкивается с множественными setState(), то из каждого вызова он извлекает передаваемый объект и соединяет их между собой. В результате получается один единственный объект, с которым в итоге и произойдет вызов setState().

В нативном Javascript слияние между собой объектов выглядит следующим образом:

const singleObject = Object.assign(
{},
objectFromSetState1,
objectFromSetState2,
objectFromSetState3
);

В Javascript правило слияния объектов гласит: если объекты имеют свойства с одинаковыми ключами(названиями), то значение ключа последнего передаваемого в Object.assign() объекта выигрывает и перезаписывает все предыдущие. Например:

const me  = {name :   "Justice"}, 
you = {name : "Your name"},
we = Object.assign({}, me, you);
we.name === "Your name"; //true
console.log(we); // {name : "Your name"}

Поскольку объект you был последним передаваемым объектом, значение “Your name” в нем перезапишет значение свойства name в предыдущем объекте.

Следовательно, повторим еще раз.

Если вы вызываете setState() с объектом несколько раз — React запустит процедуру слияния объектов в один в целях оптимизации своей работы. И тогда, если в объектах наличествуют свойства с одинаковыми ключами — в итоговый объект запишется лишь значение последнего из них.

Теперь самое время вернуться к нашей вышеупомянутой функции multiple и взглянуть на нее еще раз:


state = {score : 0};
// multiple setState() calls
increaseScoreBy3 () {
this.setState({score : this.state.score + 1});
this.setState({score : this.state.score + 1});
this.setState({score : this.state.score + 1});
}

С учетом того что мы выяснили о setState() — итоговое значение score будет не 3, а 1, потому что React не вызывает setState 3 раза, а делает из него один следующий вызов this.setState({score : this.state.score + 1});

Таким образом, вызывая setState() и передавая ему в аргументах объект мы сталкиваемся с некоторыми сложностями во всех ситуациях, когда нам нужно вычислить следующее состояние компонента на основе предыдущего. Это попросту становится небезопасно

Отсюда вывод,

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

Функциональный setState() в помощь.

Вышеприведенный пример явно показывает всю суть этой статьи, так что рекомендую тщательно ознакомиться с ним.

Как видите, когда React обнаруживает множественные вызовы функциональных setState(), вместо того чтобы объединить все эти вызовы в один он исполняет их ровно в том порядке в каком они были вызваны.

React начинает обновлять состояние приложения с каждым вызовом функционального setState(), и каждому следующему в очереди вызову он передает уже вычисленное от предыдущего.

Давайте попробуем немножно побаловаться с кодом.

Далее мы будем писать не какую-то реальную вещь, а только лишь пример чтобы продемонстрировать вам все о чем мы писали в данной статье.

Первое, давайте объявим класс компонента. После, внутри него, мы объявим фейковый setState() метод. Также мы объявим increaseScoreBy3() , который будет вызывать функциональный setState().

class User{
state = {score : 0};
//let's fake setState
setState(state, callback) {
this.state = Object.assign({}, this.state, state);
if (callback) callback();
}
// multiple functional   setState call
increaseScoreBy3 () {
this.setState( (state) => ({score : state.score + 1}) ),
this.setState( (state) => ({score : state.score + 1}) ),
this.setState( (state) => ({score : state.score + 1}) )
}
}
const Justice = new User();

Заметьте, что фейковый setState() в нашем примере принимает необязательный второй параметр — callback-функцию.

Теперь когда пользователь вызывает increaseScoreBy3(), React вызывает в порядке очереди вызовы функциональных setState().

И, наконец, давайте сымитируем процесс обновления.

// recursively update state in the order
function updateState(component, updateQueue) {
if (updateQueue.length === 1) {
return component.setState(updateQueue[0](component.state));
}
return component.setState(
updateQueue[0](component.state),
() =>
updateState( component, updateQueue.slice(1))
);
}
updateState(Justice, updateQueue);

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

Здесь я набросал немножечко кода. Поиграйтесь с ним, чтобы до конца все осознать.

После того как осознаете всю суть, придет время для того чтобы раскрыть самую большую прелесть функционального setState().

Тот самый — самый большой секрет функционального setState().

Теперь, когда мы с вами окончательно поняли как делать множественные вызовы setState(), пришло время посмотреть еще раз на определение функционального setState(), что мы давали в начале статьи и понять, что мы до сих пор его еще не раскрыли.

В течение всего времени существования React, всегда подразумевалось, что объявление или изменение состояния компонента должно осуществляться внутри него.

Но теперь мы вам покажем все прелести изменения state вне класса.

// outside your component class
function increaseScore (state, props) {
return {score : state.score + 1}
}
class User{
// inside your component class
handleIncreaseScore () {
this.setState( increaseScore)
}

}

Как видите, класс компонента больше не заботится об изменении этого состояния и мы полностью делегируем это вне него.

Это значит, что больше не нужно перегружать ваши компоненты лишним, а зачастую дублирующим кодом, ведь мы можем просто вынести это вне! Помимо этого, если логика изменения состояния компонента у вас сильно разраслась, то можно ее просто вынести в отдельный файл и наслаждаться легким к восприятию и не перегруженным кодом.

import {increaseScore} from "../stateChanges";
class User{
...
// inside your component class
handleIncreaseScore () {
this.setState( increaseScore)
}
...
}

Теперь вы можете спокойно использовать это в другом компоненте.

Как видите, все достаточно просто и элегантно. Кто знает, быть может в недалеком будущем именно этот способ объявления и изменения состояния станет превалирующим?

В любом случае, я рекомендую уже начать вам применять это и продолжать делиться с сообществом разработчиков своими открытиями.

Happy Coding!