Чому я не можу безпосередньо змінити стан компонента, насправді?


82

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

Я хотів би зрозуміти, чому, власне, я не можу просто безпосередньо змінити стан, а потім (у тій самій функції) зателефонувати, this.setState({})щоб просто викликати render.

Наприклад: Наведений нижче код, здається, працює чудово:

const React = require('react');

const App = React.createClass({
    getInitialState: function() {
        return {
            some: {
                rather: {
                    deeply: {
                        embedded: {
                            stuff: 1
                        }}}}};
    },
    updateCounter: function () {
        this.state.some.rather.deeply.embedded.stuff++;
        this.setState({}); // just to trigger the render ...
    },
    render: function() {
        return (
                <div>
                Counter value: {this.state.some.rather.deeply.embedded.stuff}
                <br></br>
                <button onClick={this.updateCounter}>inc</button>
                </div>
        );
    }
});

export default App;

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

Примітки до this.setStateдокументації в основному визначають дві проблеми:

  1. Що якщо ви мутуєте стан безпосередньо, а потім викликаєте this.setStateце, це може замінити (перезаписати?) Мутацію, яку ви зробили. Я не бачу, як це може статися у наведеному вище коді.
  2. Це setStateможе this.stateефективно мутувати в асинхронному / відкладеному режимі, і тому при доступі this.stateвідразу після виклику this.setStateви не гарантуєте доступ до остаточного мутованого стану. Я розумію, що це не проблема, якщо this.setStateце останній виклик функції оновлення.

2
Перевірте примітки в setStateдокументації . Він охоплює деякі вагомі причини.
Юя

@Kujira Я оновив своє запитання, щоб також розглянути примітки, про які ви згадали.
Маркус Юній Брут

4
окрім того, що ви думаєте, що можете цим керувати, ви просто замикаєте робочий процес фреймворку. Javascript дозволяє це робити, майте на увазі, що після того, як ви порушите шаблон, структура вже не відповідає за узгодженість вашого штату.
pietro909

4
Справа не в тому, щоб "не можна" безпосередньо мутувати стан, а в тому, "не слід".
россіпедія

Дивно, але це було задано 4 місяці тому, і досі не прийнято відповіді, чи це таке складне запитання? Насправді я не можу знайти відповіді на це за допомогою google ...
jhegedus

Відповіді:


49

Документи React дляsetState цього мають сказати:

НІКОЛИ не мутуйте this.stateбезпосередньо, оскільки дзвінок setState()після цього може замінити зроблену вами мутацію. Ставтеся this.stateтак, ніби воно незмінне.

setState()не відразу мутує, this.stateале створює очікуваний перехід стану. Доступ this.stateпісля виклику цього методу може потенційно повернути існуюче значення.

Немає гарантії синхронної роботи дзвінків, setStateі дзвінки можуть групуватися для підвищення продуктивності.

setState()завжди буде ініціювати рендеринг, якщо в ньому не реалізована логіка умовного візуалізації shouldComponentUpdate(). Якщо використовуються змінні об'єкти, і логіку неможливо реалізувати shouldComponentUpdate(), виклик setState()лише тоді, коли новий стан відрізняється від попереднього, дозволить уникнути непотрібних повторних відтворень.

В основному, якщо ви модифікуєте this.stateбезпосередньо, ви створюєте ситуацію, коли ці модифікації можуть бути перезаписані.

Пов’язане з вашими розширеними запитаннями 1) та 2), setState()не є негайним. Він ставить у чергу перехід стану на основі того, що, на його думку, відбувається, що не може включати прямі зміни доthis.state . Оскільки це ставиться в чергу, а не застосовується негайно, цілком можливо, що щось між ними модифікується таким чином, що ваші прямі зміни перезаписуються.

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


"Він перебуває у черзі переходу стану на основі того, що, на його думку, відбувається, що може не включати прямі зміни до this.state. Оскільки воно знаходиться в черзі, а не застосовується негайно, цілком можливо, що щось між ними модифікується таким чином, що ваші прямі зміни перезаписуються . " Не могли б ви детальніше пояснити, що ви маєте на увазі під цим? Наведіть приклад, коли це проблема?
jhegedus

1
@jhegedus Пориньте в кодову базу React: обробка змін стану вкрай детально описує, як setState()працюють системи зворотного виклику та зворотного виклику, і дає вам гарне уявлення про те, чому можуть виникнути проблеми.
Ouroborus

Дякуємо за підказку Ouroborus!
jhegedus

Проголосований за це: "Він перебуває у стані переходу стану на основі того, що, на його думку, відбувається, що може не включати прямі зміни до цього. Стану"
Марк Юній Брут,

62

Ця відповідь полягає у наданні достатньої кількості інформації, щоб не змінювати / не змінювати стан безпосередньо в React.

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

Потік даних React's без потоку

введіть тут опис зображення

Щоб React працював так, розробники зробили React подібним до функціонального програмування . Основним правилом функціонального програмування є незмінність . Дозвольте пояснити це голосно та чітко.

Як працює односпрямований потік?

  • states - це сховище даних, яке містить дані компонента.
  • viewКомпонента надає на основі стану.
  • Коли viewпотрібно щось змінити на екрані, це значення слід подавати з store.
  • Щоб це сталося, React надає setState()функцію, яка приймає objectnew statesі виконує порівняння та злиття (подібне до object.assign()) попереднього стану та додає новий стан до сховища даних стану.
  • Кожного разу, коли дані у сховищі стану змінюються, реакція викликає повторний візуалізацію з новим станом, який viewспоживає, і відображає його на екрані.

Цей цикл триватиме протягом усього життя компонента.

Якщо ви бачите наведені вище кроки, це ясно показує, що багато чого відбувається позаду, коли ви змінюєте стан. Отже, коли ви безпосередньо мутуєте стан і викликаєте setState()порожній об'єкт. Вода previous stateбуде забруднена вашою мутацією. Через що неглибоке порівняння та злиття двох станів буде порушено або не відбудеться, оскільки зараз у вас буде лише один стан. Це порушить усі методи життєвого циклу React.

Як результат, ваш додаток буде вести себе ненормально або навіть аварійно працювати. У більшості випадків це не вплине на ваш додаток, оскільки всі додатки, які ми використовуємо для тестування, досить малі.

І ще одним недоліком мутації Objectsта Arraysв JavaScript є те, що коли ви призначаєте об'єкт або масив, ви просто робите посилання на цей об'єкт або цей масив. Коли ви його мутуєте, це вплине на все посилання на цей об’єкт або на цей масив. React обробляє це розумно у фоновому режимі і просто надає нам API, щоб він працював.

Найпоширеніші помилки, допущені під час обробки станів у React

// original state
this.state = {
  a: [1,2,3,4,5]
}

// changing the state in react
// need to add '6' in the array

// bad approach
const b = this.state.a.push(6)
this.setState({
  a: b
}) 

У наведеному вище прикладі this.state.a.push(6)буде мутувати стан безпосередньо. Присвоєння його іншій змінній та виклик setState- те саме, що показано нижче. Оскільки ми все одно мутували стан, немає сенсу призначати його іншій змінній і викликати за setStateдопомогою цієї змінної.

// same as 
this.state.a.push(6)
this.setState({})

Більшість людей робить це. Це так неправильно . Це порушує красу React, і це зробить вас поганим програмістом.

Отже, який найкращий спосіб обробляти стани в React? Дозволь пояснити.

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

// original state
this.state = {
  a: [1,2,3,4,5]
}

// changing the state in react
// need to add '6' in the array

// create a copy of this.state.a
// you can use ES6's destructuring or loadash's _.clone()
const currentStateCopy = [...this.state.a]

Тепер мутація currentStateCopyне змінить початковий стан. Виконайте операції currentStateCopyта встановіть його як новий стан за допомогою setState().

currentStateCopy.push(6)
this.state({
  a: currentStateCopy
})

Це красиво, правда?

Роблячи це, всі посилання на this.state.aне впливатимуть, поки ми не використаємо setState. Це дає вам можливість контролювати свій код, і це допоможе вам написати елегантний тест і зробити вас впевненими у роботі коду у виробництві.

Щоб відповісти на ваше запитання,

Чому я не можу безпосередньо змінити стан компонента?


Так, ти можеш . Але вам доведеться зіткнутися з наступними наслідками.

  1. Під час масштабування ви будете писати некерований код.
  2. Ви втратите контроль над stateусіма компонентами.
  3. Замість того, щоб використовувати React, ви будете писати власні коди над React.

Незмінність не є необхідною річчю, оскільки JavaScript є однопоточним. Але, добре дотримуватися практики, яка допоможе вам у довгостроковій перспективі.

PS. Я написав близько 10000 рядків змінного коду React JS. Якщо це зламається зараз, я не знаю, де шукати, тому що всі значення десь мутують. Коли я це зрозумів, я почав писати незмінний код. Довірся мені! Це найкраще, що ви можете зробити з продуктом або додатком.

Сподіваюся, це допомагає!


2
Тільки нагадуємо: більшість основних методів клонування в JS ( sliceдеструктуризація ES6 тощо) є неглибокими. Якщо у вас є вкладений масив або вкладені об’єкти, вам доведеться розглянути інші методи глибокого копіювання, наприклад JSON.parse(JSON.stringify(obj))(хоча цей конкретний метод не буде працювати, якщо ваш об’єкт має кругові посилання).
Гален Лонг

2
@Galen Long буде _.cloneDeepдосить
Джош Бродхерст

2
@JoshBroadhurst Чудовий момент, так би. І якщо хтось турбується про встановлення всього пакету Lodash лише для однієї функції, ви можете встановити лише модуль cloneDeep з npm install lodash.clonedeepі використовувати його з let cloneDeep = require("lodash.clonedeep");.
Гален Лонг

5

найпростіша відповідь на "

Чому я не можу безпосередньо змінити стан компонента:

це все про етап оновлення.

коли ми оновлюємо стан компонента, усі його діти також будуть відображені. або відтворено все наше дерево компонентів.

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

Потім React подивиться на віртуальний DOM, він також має копію старого віртуального DOM, тому ми не повинні оновлювати стан безпосередньо , тому ми можемо мати два різних посилання на об'єкти в пам'яті, ми маємо старий віртуальний DOM як а також новий віртуальний DOM.

тоді реакція зрозуміє, що змінилося, і виходячи з цього вона відповідно оновить реальний DOM.

сподіваюся, це допоможе.



1

setState запускає рендеринг компонентів. коли ми хочемо знову і знову оновлювати стан, нам потрібно встановити setState, інакше він не працює належним чином.


1

Мене дивує, що жодна з поточних відповідей не говорить про чисті компоненти. Ці компоненти повторно відтворюють лише тоді, коли виявлено зміну в одному з реквізитів.

Скажімо, ви безпосередньо мутуєте стан і передаєте не значення, а об’єкт перенапруги компоненту нижче. Цей об'єкт все ще має те саме посилання, що і попередній об'єкт, що означає, що компоненти pure / memo не будуть повторно відтворюватися, навіть якщо ви мутували одне з властивостей.

Оскільки ви не завжди знаєте, з яким типом компонента працюєте, імпортуючи їх із бібліотек, це ще одна причина дотримуватися правила немутації.

Ось приклад такої поведінки в дії (за допомогою R.evolveспрощення створення копії та оновлення вкладеного вмісту):

class App extends React.Component {
  state = {some: {rather: {deeply: {nested: {stuff: 1}}}}};
  
  mutatingIncrement = () => {
    this.state.some.rather.deeply.nested.stuff++;
    this.setState({});
  }
  nonMutatingIncrement = () => {
    this.setState(R.evolve(
      {some: {rather: {deeply: {nested: {stuff: n => n + 1}}}}}
    ));
  }

  render() {
    return <div>
      Pure Component: <PureCounterDisplay {...this.state} />
      <br />
      Normal Component: <CounterDisplay {...this.state} />
      <br />
      <button onClick={this.mutatingIncrement}>mutating increment</button>
      <button onClick={this.nonMutatingIncrement}>non-mutating increment</button>
    </div>;
  }
}

const CounterDisplay = (props) => (
  <React.Fragment>
    Counter value: {props.some.rather.deeply.nested.stuff}
  </React.Fragment>
);
const PureCounterDisplay = React.memo(CounterDisplay);

ReactDOM.render(<App />, document.querySelector("#root"));
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/ramda@0/dist/ramda.min.js"></script>
<div id="root"></div>


-3

Моє теперішнє розуміння базується на цій та цій відповіді:

Якщо ви не використовуєте shouldComponentUpdate або які - небудь інші методи життєвого циклу (як componentWillReceiveProps, componentWillUpdateі componentDidUpdate) , де ви порівняти старі і нові підпори / стан

ТОДІ

його штраф можна мутувати, stateа потім викликати setState(), інакше це не добре.


7
Зверніть увагу, що це стосується лише поточної та попередньої версій React (на даний час на v15.3.2). Хто знає, чи не зламається ваш код у майбутній версії, оскільки він покладається на поведінку, яка задокументована як заборонена.
TomW
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.