this.setState не є зливаються станами, як я б очікував


160

Я маю такий стан:

this.setState({ selected: { id: 1, name: 'Foobar' } });  

Потім я оновлюю стан:

this.setState({ selected: { name: 'Barfoo' }});

Оскільки я setStateприпускаю злиття, я б очікував, що це буде:

{ selected: { id: 1, name: 'Barfoo' } }; 

Але замість цього він їсть id, і стан є:

{ selected: { name: 'Barfoo' } }; 

Це очікувана поведінка і яке рішення для оновлення лише одного властивості вкладеного об'єкта стану?

Відповіді:


143

Я думаю setState(), що рекурсивне злиття не робить.

Ви можете використовувати значення поточного стану, this.state.selectedщоб побудувати новий стан, а потім зателефонувати setState()за цим:

var newSelected = _.extend({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });

Тут я використовував функціональну _.extend()функцію (з бібліотеки underscore.js), щоб запобігти внесенню змін у існуючу selectedчастину стану шляхом створення дрібної його копії.

Іншим рішенням було б написати, setStateRecursively()який рекурсивно зливається в новому стані, а потім викликає replaceState()з ним:

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}

7
Чи працює це надійно, якщо ви робите це не один раз або є ймовірність, що реакція поставить в чергу чергові дзвінки заміни, і останній виграє?
edoloughlin

111
Для людей, які вважають за краще використовувати розширення та які також використовують вавило, що ви можете зробити, this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })що перекладається наthis.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
Pickels

8
@Pickels це чудово, ідіоматично для React і не вимагає underscore/ lodash. Скрутіть це на власну відповідь, будь ласка.
ericsoco

14
this.state не обов'язково актуальні . Ви можете отримати несподівані результати, використовуючи метод розширення. Передача функції оновлення до setState- це правильне рішення.
Стром

1
@ EdwardD'Souza Це бібліотека lodash. Його зазвичай викликають як підкреслення, так само, як jQuery зазвичай називають доларовим знаком. (Отже, це _.extend ().)
mjk

96

Нещодавно до React.addons були додані помічники щодо незмінюваності, тож з цим ви тепер можете зробити щось на кшталт:

var newState = React.addons.update(this.state, {
  selected: {
    name: { $set: 'Barfoo' }
  }
});
this.setState(newState);

Документація помічників щодо непорушності .


7
оновлення застаріло. facebook.github.io/react/docs/update.html
thedanotto

3
@thedanotto Схоже, React.addons.update()метод застарілий, але бібліотека заміщення github.com/kolodny/immutability-helper містить еквівалентну update()функцію.
Bungle

37

Оскільки багато відповідей використовують сучасний стан як основу для об'єднання нових даних, я хотів зазначити, що це може зламатись. Зміни стану ставлять у чергу, і вони не змінюють негайно об'єкт стану компонента. Дані про стан посилань перед обробкою черги, таким чином, дадуть вам застарілі дані, які не відображають очікувані зміни, внесені в setState. З документів:

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

Це означає, що використання "поточного" стану в якості посилання в наступних дзвінках до setState недостовірне. Наприклад:

  1. Перший виклик до setState, в черзі зміни об'єкта стану
  2. Другий дзвінок для setState. Ваш стан використовує вкладені об'єкти, тому ви хочете здійснити об'єднання. Перед тим, як викликати setState, ви отримуєте об'єкт поточного стану. Цей об'єкт не відображає змін в черзі, здійснених під час першого дзвінка до setState, вище, оскільки це все ще початковий стан, який зараз слід вважати "несвіжим".
  3. Виконайте злиття. Результат є початковим "несвіжим" станом плюс нові дані, які ви тільки що встановили, зміни від початкового виклику setState не відображаються. Ваші черги викликів setState цієї другої зміни.
  4. Черга реагує на процеси. Перший виклик setState обробляється, стан оновлення. Другий набірдержавного виклику обробляється, стан оновлення. Об'єкт другого setState тепер замінив перший, і оскільки дані, які ви мали під час здійснення цього дзвінка, були черсткими, модифіковані застарілі дані цього другого виклику клобували зміни, зроблені в першому виклику, які втрачаються.
  5. Коли черга порожня, React визначає, чи потрібно відображати і т. Д. У цей момент ви виведете зміни, зроблені в другому виклику setState, і це буде так, ніби перший виклик setState ніколи не відбувся.

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


5
Чи є приклад чи кілька документів про те, як це зробити? afaik, ніхто це не враховує.
Йозеф.B

@Денніс Спробуйте мою відповідь нижче.
Мартін Доусон

Я не бачу, де проблема. Те, що ви описуєте, відбудеться лише в тому випадку, якщо ви спробуєте отримати доступ до "нового" стану після виклику setState. Це зовсім інша проблема, яка не має нічого спільного з питанням про ОП. Доступ до старого стану перед тим, як викликати setState, щоб ви могли сконструювати новий об'єкт стану, ніколи не буде проблемою, і насправді саме це очікує React.
nextgentech

@nextgentech "Доступ до старого стану перед тим, як викликати setState, щоб ви могли побудувати новий об'єкт стану, ніколи не буде проблемою" - ви помиляєтесь. Ми говоримо про стан перезапису на основі this.state, яке може мати неактивні (а точніше, оновлення в черзі), коли ви отримуєте доступ до нього.
Madbreaks

1
@Madbreaks Добре, так, після перечитування офіційних документів я бачу, що зазначено, що ви повинні використовувати форму функції setState для надійного оновлення стану на основі попереднього стану. При цьому я ніколи не стикався з проблемою, якщо не намагався кілька разів викликати setState в одній і тій же функції. Дякую за відповіді, які змусили мене перечитати характеристики.
nextgentech

10

Я не хотів встановлювати іншу бібліотеку, ось ось ще одне рішення.

Замість:

this.setState({ selected: { name: 'Barfoo' }});

Зробіть це замість цього:

var newSelected = Object.assign({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });

Або, завдяки @ icc97 у коментарях, ще більш лаконічно, але, можливо, менш читабельно:

this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });

Крім того, щоб бути зрозумілим, ця відповідь не порушує жодних проблем, які @bgannonpl згадали вище.


Оновити, перевірити. Просто цікаво, чому б не зробити так? this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
Юстус Ромійн

1
@JustusRomijn На жаль, тоді ви будете безпосередньо змінювати поточний стан. Object.assign(a, b)бере атрибути з b, вставляє / перезаписує їх aі потім повертає a. Реагуйте, що люди отримують знущання, якщо ви змінюєте стан безпосередньо.
Райан Шиллінгтон

Саме так. Просто зауважте, що це працює лише для дрібної копіювання / призначення.
Madbreaks

1
@JustusRomijn Ви можете зробити це на MDN правонаступника в одному рядку , якщо ви додасте в {}якості першого аргументу: this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });. Це не мутує вихідний стан.
icc97

@ icc97 Приємно! Я додав це до своєї відповіді.
Райан Шиллінгтон

8

Збереження попереднього стану на основі @bgannonpl відповіді:

Приклад Лодаша :

this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }));

Щоб переконатися, що він працює належним чином, ви можете використовувати зворотний виклик функції другого параметра:

this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }), () => alert(this.state.selected));

Я використовував, mergeтому що extendвідкидає інші властивості інакше.

Приклад незмінюваності реагування :

import update from "react-addons-update";

this.setState((previousState) => update(previousState, {
    selected:
    { 
        name: {$set: "Barfood"}
    }
});

2

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

Оскільки встановлення стану в глибині є звичайною ситуацією, я створив наступний міксин:

var SeStateInDepthMixin = {
   setStateInDepth: function(updatePath) {
       this.setState(React.addons.update(this.state, updatePath););
   }
};

Цей міксин входить до більшості моїх компонентів, і я, як правило, більше не використовую setState.

За допомогою цього міксину все, що вам потрібно зробити для досягнення бажаного ефекту - викликати функцію setStateinDepthнаступним чином:

setStateInDepth({ selected: { name: { $set: 'Barfoo' }}})

Для отримання додаткової інформації:


Вибачте за пізню відповідь, але ваш приклад Mixin здається цікавим. Однак якщо у мене є 2 вкладені стани, наприклад, this.state.test.one та this.state.test.two. Чи правильно, що лише одна з них буде оновлена, а інша буде видалена? Коли я
оновлюю

гіп @mamruoc, this.state.test.twoвсе ще буде там після поновлення з this.state.test.oneдопомогою setStateInDepth({test: {one: {$set: 'new-value'}}}). Я використовую цей код у всіх своїх компонентах, щоб обійти обмежену setStateфункцію React .
Доріан

1
Я знайшов рішення. Я ініціював this.state.testяк масив. Змінившись на Об’єкти, вирішив це.
mamruoc

2

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

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

export default class StateWrapper {

    constructor(setState, initialProps = []) {
        this.setState = props => {
            this.state = {...this.state, ...props}
            setState(this.state)
        }
        this.props = initialProps
    }

    render() {
        return(<div>render() not defined</div>)
    }

    component = props => {
        this.props = {...this.props, ...props}
        return this.render()
    }
}

Тоді для кожного складного властивості у верхньому стані я створюю один клас StateWrapped. Тут ви можете встановити реквізити за замовчуванням у конструкторі, і вони будуть встановлені, коли клас ініціалізований, ви можете звернутися до місцевого стану для значень та встановити локальний стан, посилатися на локальні функції та передати його в ланцюг:

class WrappedFoo extends StateWrapper {

    constructor(...props) { 
        super(...props)
        this.state = {foo: "bar"}
    }

    render = () => <div onClick={this.props.onClick||this.onClick}>{this.state.foo}</div>

    onClick = () => this.setState({foo: "baz"})


}

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

class TopComponent extends React.Component {

    constructor(...props) {
        super(...props)

        this.foo = new WrappedFoo(
            props => this.setState({
                fooProps: props
            }) 
        )

        this.foo2 = new WrappedFoo(
            props => this.setState({
                foo2Props: props
            }) 
        )

        this.state = {
            fooProps: this.foo.state,
            foo2Props: this.foo.state,
        }

    }

    render() {
        return(
            <div>
                <this.foo.component onClick={this.onClickFoo} />
                <this.foo2.component />
            </div>
        )
    }

    onClickFoo = () => this.foo2.setState({foo: "foo changed foo2!"})
}

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


2

На даний момент,

Якщо наступний стан залежить від попереднього стану, рекомендуємо замість цього скористатися формою функції оновлення:

відповідно до документації https://reactjs.org/docs/react-component.html#setstate , використовуючи:

this.setState((prevState) => {
    return {quantity: prevState.quantity + 1};
});

Дякуємо за посилання / посилання, яке не було в інших відповідях.
Madbreaks

1

Рішення

Редагувати: це рішення використовувалося для використання синтаксису поширення . Метою було зробити об’єкт без будь-яких посилань на нього prevState, щоб prevStateйого не змінили. Але в моєму використанні, prevStateздається, іноді змінювали. Отже, для ідеального клонування без побічних ефектів ми переходимо prevStateдо JSON, а потім знову. (Натхнення використовувати JSON надійшло від MDN .)

Пам'ятайте:

Кроки

  1. Зробіть копію власності кореневого рівня в stateтому , що ви хочете змінити
  2. Змініть цей новий об’єкт
  3. Створіть об’єкт оновлення
  4. Повернути оновлення

Етапи 3 і 4 можна поєднувати по одній лінії.

Приклад

this.setState(prevState => {
    var newSelected = JSON.parse(JSON.stringify(prevState.selected)) //1
    newSelected.name = 'Barfoo'; //2
    var update = { selected: newSelected }; //3
    return update; //4
});

Спрощений приклад:

this.setState(prevState => {
    var selected = JSON.parse(JSON.stringify(prevState.selected)) //1
    selected.name = 'Barfoo'; //2
    return { selected }; //3, 4
});

Це добре відповідає рекомендаціям React. На основі відповіді eicksl на подібне запитання.


1

Рішення ES6

Ми встановлюємо стан спочатку

this.setState({ selected: { id: 1, name: 'Foobar' } }); 
//this.state: { selected: { id: 1, name: 'Foobar' } }

Ми змінюємо властивість на певному рівні державного об’єкта:

const { selected: _selected } = this.state
const  selected = { ..._selected, name: 'Barfoo' }
this.setState({selected})
//this.state: { selected: { id: 1, name: 'Barfoo' } }

0

Стан реагування не виконує рекурсивного злиття, setStateхоча очікує, що одночасно не буде оновлених членів стану. Вам або доведеться копіювати вкладені об'єкти / масиви самостійно (з array.slice або Object.assign) або використовувати виділену бібліотеку.

Як ця. NestedLink безпосередньо підтримує обробку стану сполуки React.

this.linkAt( 'selected' ).at( 'name' ).set( 'Barfoo' );

Крім того, посилання на selectedабо selected.nameможе передаватися скрізь як окрема опора та модифікована там set.


-2

Ви встановили початковий стан?

Я використаю, наприклад, свій власний код:

    getInitialState: function () {
        return {
            dragPosition: {
                top  : 0,
                left : 0
            },
            editValue : "",
            dragging  : false,
            editing   : false
        };
    }

У додатку, над яким я працюю, саме так я встановлював і використовую стан. Я вірю, що setStateви можете просто редагувати всі стани, які ви хочете окремо. Я називав це так:

    onChange: function (event) {
        event.preventDefault();
        event.stopPropagation();
        this.setState({editValue: event.target.value});
    },

Майте на увазі, що ви повинні встановити стан у межах React.createClassфункції, яку ви викликалиgetInitialState


1
який міксин ви використовуєте у своєму компоненті? Це не працює для мене
Сачин

-3

Я використовую tmp var для зміни.

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}

2
Тут tmp.theme = v - стан, що мутує. Тож це не рекомендований спосіб.
almoraleslopez

1
let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} Не робіть цього, навіть якщо це ще не дало вам проблем.
Кріс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.