Функціональний setState - це майбутнє React

Оновлення: Я провів наступну бесіду на цю тему на React Rally. У той час як цей пост більше про «функціональної SetState» моделі, мова йде більше про розуміння SetState глибоко

React популяризував функціональне програмування на JavaScript. Це призвело до того, що гігантські фреймворки прийняли шаблон інтерфейсу на основі компонентів, який використовує React. І зараз функціональна лихоманка перекидається на екосистему веб-розробки в цілому.

Але команда React далека від того, щоб змиритися. Вони продовжують копати глибше, відкриваючи ще більш функціональні дорогоцінні камені, заховані в легендарній бібліотеці.

Тож сьогодні я відкриваю вам нове функціональне золото, поховане в React, яке найкраще зберігається в секреті React - Функціональний набірState!

Гаразд, я щойно вигадав це ім’я ... і воно не зовсім нове чи таємне. Ні, не зовсім так. Дивіться, це шаблон, вбудований у React, який відомий лише небагатьом розробникам, які справді заглибились. І вона ніколи не мала назви. Але тепер це так - функціональний setState!

Описуючи слова Дана Абрамова, описуючи цей шаблон, Functional setState - це шаблон, де ви

"Оголошувати зміни стану окремо від класів компонентів."

А?

Гаразд ... те, що ти вже знаєш

React - це бібліотека інтерфейсу на основі компонентів. Компонент - це в основному функція, яка приймає деякі властивості і повертає елемент інтерфейсу.

function User(props) { return ( A pretty user );}

Можливо, компонент повинен мати свій стан і керувати ним. У такому випадку ви зазвичай пишете компонент як клас. Тоді ви маєте його стан у реальному часі у функції класу constructor:

class User { constructor () { this.state = { score : 0 }; }
 render () { return ( This user scored {this.state.score} ); }}

Для управління станом React пропонує спеціальний метод, який називається setState(). Ви використовуєте його так:

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

Зверніть увагу, як це setState()працює. Ви передаєте йому об’єкт, що містить частини (частини) стану, який потрібно оновити. Іншими словами, об’єкт, який ви передаєте, матиме ключі, що відповідають ключам у стані компонента, а потім setState()оновлює або встановлює стан шляхом об’єднання об’єкта в стан. Таким чином, “держава-набір”.

Чого ви, мабуть, не знали

Пам'ятаєте, як ми сказали, що це setState()працює? Ну, а якби я сказав вам, що замість передачі об’єкта ви можете передати функцію ?

Так. setState()також приймає функцію. Функція приймає попередній стан та поточний опис компонента, який вона використовує для обчислення та повернення наступного стану. Дивіться це нижче:

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

Зверніть увагу, що setState()це функція, і ми передаємо їй іншу функцію (функціональне програмування ... функціональний setState ). На перший погляд, це може здатися потворним, занадто багато кроків лише для встановлення стану. Чому ти коли-небудь захочеш це зробити?

Навіщо передавати функцію setState?

Справа в тому, що оновлення стану можуть бути асинхронними.

Подумайте, що відбувається, коли setState()викликають. React спочатку об’єднає об’єкт, якому ви передали, setState()у поточний стан. Тоді це почне процес примирення . Він створить нове дерево React Element (представлення об’єкта вашого інтерфейсу користувача), порівняє нове дерево зі старим деревом, з’ясує, що змінилося на основі об’єкта, якому ви передали setState(), і нарешті оновить DOM.

Ух! Стільки роботи! Насправді це навіть надто спрощений резюме. Але довіряйте React!

React - це не просто “встановлений стан”.

Через велику кількість роботи, дзвінки setState()можуть не відразу оновити ваш стан.

React може групувати кілька setState()дзвінків в одне оновлення для підвищення продуктивності.

Що означає React під цим?

По-перше, " кілька setState()викликів" може означати виклик setState()всередині однієї функції більше одного разу, наприклад:

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

Тепер, коли React стикається з " кількома setState()дзвінками", замість того , щоб робити це "set-state" цілих три рази , React уникне тієї величезної кількості роботи, яку я описав вище, і розумно скаже собі: "Ні! Я не збираюся підніматися на цю гору тричі, несучи та оновлюючи якийсь шматочок штату під час кожної окремої подорожі. Ні, я волів би взяти контейнер, упакувати всі ці фрагменти разом і зробити це оновлення лише один раз ». І це, друзі мої, єдозування !

Пам’ятайте, що те, до чого ви переходите, setState()є звичайним об’єктом. Тепер, припустимо, що в будь-який час React зустрічає " кілька setState()викликів", він виконує пакетну операцію, витягуючи всі об'єкти, що передаються кожному 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"}

Because you are the last object merged into we, the value of name in the you object — “Your name” — overrides the value of name in the me object. So “Your name” makes it into the we object… you win! :)

Thus, if you call setState() with an object multiple times — passing an object each time — React will merge. Or in other words, it will compose a new object out of the multiple objects we passed it. And if any of the objects contains the same key, the value of the key of the last object with same key is stored. Right?

Це означає, що, враховуючи нашу increaseScoreBy3функцію вище, кінцевий результат функції буде просто 1 замість 3, оскільки React не відразу оновив стан у тому порядку, який ми викликали setState(). Але спочатку React склав усі об’єкти разом, що призводить до цього:, {score : this.state.score + 1}потім лише один раз зробив “set-state” - із нещодавно складеним об’єктом. Що - щось на зразок цього: User.setState({score : this.state.score + 1}.

Якщо бути надзвичайно чітким, передача об’єкта setState()не є проблемою тут. Справжня проблема полягає в передачі об'єкта, setState()коли потрібно обчислити наступний стан з попереднього стану. Тож припиніть це робити. Це не безпечно!

Оскільки this.propsі this.stateможуть бути оновлені асинхронно, не слід покладатися на їх значення для обчислення наступного стану.

Ось ручка Софії Швець, яка демонструє цю проблему. Пограйте з цим і зверніть увагу як на погані, так і на хороші рішення цього пера:

Функціональний набір

Якщо ви не витратили час на гру з ручкою вище, я настійно рекомендую вам це зробити, оскільки це допоможе вам зрозуміти основну концепцію цього допису.

Поки ви грали з ручкою вище, ви, без сумніву, бачили, що функціональний setState вирішив нашу проблему. Але як саме?

Давайте проконсультуємось з Oprah of React - Ден.

Зверніть увагу на відповідь, яку він дав. Коли ви робите функціональний setState ...

Оновлення будуть поставлені в чергу та пізніше виконані в тому порядку, в якому вони були викликані.

So, when React encounters “multiple functional setState() calls” , instead of merging objects together, (of course there are no objects to merge) React queues the functions “in the order they were called.”

After that, React goes on updating the state by calling each functions in the “queue”, passing them the previous state — that is, the state as it was before the first functional setState() call (if it’s the first functional setState() currently executing) or the state with the latest update from the previous functional setState() call in the queue.

Again, I think seeing some code would be great. This time though, we’re gonna fake everything. Know that this is not the real thing, but is instead just here to give you an idea of what React is doing.

Also, to make it less verbose, we’ll use ES6. You can always write the ES5 version later if you want.

First, let’s create a component class. Then, inside it, we’ll create a fake setState() method. Also, our component would have a increaseScoreBy3()method, which will do a multiple functional setState. Finally, we’ll instantiate the class, just as React would do.

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

Note that setState also accepts an optional second parameter — a callback function. If it’s present React calls it after updating the state.

Now when a user triggers increaseScoreBy3(), React queues up the multiple functional setState. We won’t fake that logic here, as our focus is on what actually makes functional setState safe. But you can think of the result of that “queuing” process to be an array of functions, like this:

const updateQueue = [ (state) => ({score : state.score + 1}), (state) => ({score : state.score + 1}), (state) => ({score : state.score + 1})];

Finally, let’s fake the updating process:

// recursively update state in the orderfunction 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);

True, this is not as so sexy a code. I trust you could do better. But the key focus here is that every time React executes the functions from your functional setState, React updates your state by passing it a fresh copy of the updated state. That makes it possible for functional setState to set state based on the previous state.

Here I made a bin with the complete code. Tinker around it (possibly make it look sexier), just to get more sense of it.

FunctionalSetStateInAction

A Play with the code in this bin will be fun. Remember! we’re just faking React to get the idea...jsbin.com

Play with it to grasp it fully. When you come back we’re gonna see what makes functional setState truly golden.

The best-kept React secret

So far, we’ve deeply explored why it’s safe to do multiple functional setStates in React. But we haven’t actually fulfilled the complete definition of functional setState: “Declare state changes separately from the component classes.”

Over the years, the logic of setting-state — that is, the functions or objects we pass to setState() — have always lived inside the component classes. This is more imperative than declarative.

Well today, I present you with newly unearthed treasure — the best-kept React secret:

Thanks to Dan Abramov!

That is the power of functional setState. Declare your state update logic outside your component class. Then call it inside your component class.

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

This is declarative! Your component class no longer cares how the state updates. It simply declares the type of update it desires.

To deeply appreciate this, think about those complex components that would usually have many state slices, updating each slice on different actions. And sometimes, each update function would require many lines of code. All of this logic would live inside your component. But not anymore!

Also, if you’re like me, I like keeping every module as short as possible, but now you feel like your module is getting too long. Now you have the power to extract all your state change logic to a different module, then import and use it in your component.

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

Now you can even reuse the increaseScore function in a different component. Just import it.

What else can you do with functional setState?

Make testing easy!

You can also pass extra arguments to calculate the next state (this one blew my mind… #funfunFunction).

Expect even more in…

The Future of React

For years now, the react team has been experimenting with how to best implement stateful functions.

Functional setState seems to be just the right answer to that (probably).

Hey, Dan! Any last words?

If you’ve made it this far, you’re probably as excited as I am. Start experimenting with this functional setStatetoday!

If you feel like I’ve done any nice job, or that others deserve a chance to see this, kindly click on the green heart below to help spread a better understanding of React in our community.

If you have a question that hasn’t been answered or you don’t agree with some of the points here feel free to drop in comments here or via Twitter.

Happy Coding!