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' } }; 

これは予想される動作ですか、ネストされた状態オブジェクトの1つのプロパティのみを更新するための解決策は何ですか?

回答:


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
これを複数回実行した場合、またはreplaceState呼び出しをキューに入れて最後の呼び出しが勝つ可能性がある場合、これは確実に機能しますか?
エドロフリン2014

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 2015年

14
this.state 必ずしも最新ではありません。あなたはextendメソッドを使用して予期しない結果を得ることができます。更新関数を渡すことsetStateは正しい解決策です。
Strom、2016年

1
@ EdwardD'Souza lodashライブラリです。これは通常、jQueryがドル記号として呼び出されるのと同じように、アンダースコアとして呼び出されます。(つまり、_。extend()です。)
mjk

96

不変性ヘルパーが最近React.addonsに追加されたので、これで次のようなことができるようになります。

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

不変性ヘルパーのドキュメント



3
@thedanotto React.addons.update()メソッドは非推奨のようですが、置換ライブラリgithub.com/kolodny/immutability-helperには同等のupdate()関数が含まれています。
バングル2017

37

回答の多くは現在の状態を新しいデータのマージの基礎として使用しているため、これが壊れる可能性があることを指摘したかった。状態変更はキューに入れられ、コンポーネントの状態オブジェクトをすぐには変更しません。したがって、キューが処理される前に状態データを参照すると、setStateで行った保留中の変更を反映していない古いデータが得られます。ドキュメントから:

setState()はthis.stateをすぐには変更しませんが、保留中の状態遷移を作成します。このメソッドを呼び出した後にthis.stateにアクセスすると、既存の値が返される可能性があります。

つまり、以降のsetStateの呼び出しで「現在の」状態を参照として使用することは信頼できません。例えば:

  1. 最初にsetStateを呼び出し、状態オブジェクトへの変更をキューに入れます
  2. setStateの2回目の呼び出し。状態はネストされたオブジェクトを使用するため、マージを実行します。setStateを呼び出す前に、現在の状態オブジェクトを取得します。このオブジェクトは、上記のsetStateへの最初の呼び出しでキューに入れられた変更を反映していません。これは、元の状態であり、「古い」と見なされる必要があるためです。
  3. マージを実行します。結果は、元の「古い」状態と、設定したばかりの新しいデータです。最初のsetState呼び出しからの変更は反映されません。setState呼び出しは、この2番目の変更をキューに入れます。
  4. Reactプロセスキュー。最初のsetState呼び出しが処理され、状態が更新されます。2番目のsetState呼び出しが処理され、状態が更新されます。2番目のsetStateのオブジェクトが最初のオブジェクトに置き換わりました。その呼び出しを行ったときのデータが古くなっているため、この2番目の呼び出しからの変更された古いデータは、最初の呼び出しで行われた変更を破棄し、失われます。
  5. キューが空の場合、Reactはレンダリングするかどうかなどを決定します。この時点で、2番目のsetState呼び出しで行われた変更をレンダリングし、最初のsetState呼び出しが行われなかったかのようになります。

現在の状態を使用する必要がある場合(ネストされたオブジェクトにデータをマージする場合など)、setStateはオブジェクトの代わりに関数を引数として受け入れます。この関数は、以前の状態の更新後に呼び出され、状態を引数として渡します。これにより、以前の変更を確実に反映するアトミックな変更を行うことができます。


5
これを行う方法の例やドキュメントはありますか?アファイク、誰もこれを考慮に入れていない。
Josef.B 2016

@デニス以下の私の答えを試してください。
Martin Dawson

問題がどこにあるのかわかりません。説明している内容は、setStateを呼び出した後で「新しい」状態にアクセスしようとした場合にのみ発生します。これは、OPの質問とは何の関係もない、まったく別の問題です。新しい状態オブジェクトを構築できるようにsetStateを呼び出す前に古い状態にアクセスすることは問題になりません。実際、Reactが期待するとおりです。
nextgentech '15年

@nextgentech "新しい状態オブジェクトを構築できるようにsetStateを呼び出す前に古い状態にアクセスすることは問題になりません" -間違いです。に基づいて状態を上書きすることについて話していますthis.state。これは、アクセス時に陳腐化している(またはキューに入れられた更新が保留されている)可能性があります。
Madbreaks、2018年

1
@Madbreaksわかりました、はい、公式ドキュメントをもう一度読んだところ、以前の状態に基づいて状態を確実に更新するには、setStateの関数形式を使用する必要があることがわかりました。つまり、同じ関数でsetStateを複数回呼び出そうとしない限り、これまでに問題に遭遇したことはありません。仕様を再確認するようにしてくれた回答に感謝します。
nextgentech 2018年

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" }));
Justus Romijn 2017

1
@JustusRomijn残念ながら、現在の状態を直接変更することになります。 Object.assign(a, b)から属性をb取得し、それらを挿入/上書きしてaからを返しますa。状態を直接変更すると、Reactの人々は高揚します。
ライアンシリントン

丁度。これは、浅いコピー/割り当てに対してのみ機能することに注意してください。
Madbreaks、2018年

1
@JustusRomijn 最初の引数として追加すると、MDN割り当てごとに1行でこれを行うことができます。これは元の状態を変更しません。{}this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });
icc97 2018

@ icc97いいね!私はそれを私の答えに加えました。
ライアン

8

@bgannonplの回答に基づいて以前の状態を保持する:

ロダッシュの例:

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

それが正しく機能していることを確認するには、2番目のパラメーター関数コールバックを使用できます。

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

それ以外の場合は他のプロパティを破棄するmergeので使用しましたextend

React不変性の例:

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の例は興味深いようです。ただし、たとえばthis.state.test.oneとthis.state.test.twoのように2つのネストされた状態がある場合。これらの1つだけが更新され、他の1つが削除されることは正しいですか?最初のものを更新したときに、2番目のものが状態にある場合、それは削除されます...
mamruoc

hy @mamruoc、this.state.test.twothis.state.test.one使用して更新した後も存在しますsetStateInDepth({test: {one: {$set: 'new-value'}}})。Reactの制限されたsetState機能を回避するために、すべてのコンポーネントでこのコードを使用しています。
ドリアン

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クラスを1つ作成します。ここでコンストラクタにデフォルトの小道具を設定できます。これらはクラスの初期化時に設定され、値のローカル状態を参照してローカル状態を設定し、ローカル関数を参照して、チェーンに渡すことができます。

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!"})
}

私の目的には非常にうまく機能しているようですが、ラップされた各コンポーネントは独自の状態を追跡していますが、トップコンポーネントの状態を更新しているため、トップレベルのコンポーネントでラップされたコンポーネントに割り当てるプロパティの状態を変更することはできませんそれが変わるたびに。



1

解決

編集:このソリューションは、スプレッド構文を使用するために使用されていました。目標は、への参照せずにオブジェクトを作成しましたprevStateので、それはprevState修正されません。しかし、私の使い方では、prevState時々変更されるように見えました。そのため、副作用のない完全なクローンを作成するためにprevState、JSONに変換してから再び戻します。(JSONを使用するためのインスピレーションはMDNから来ました。)

覚えておいてください:

手順

  1. 変更するルートレベルのプロパティのコピーを作成stateします
  2. この新しいオブジェクトを突然変異させる
  3. 更新オブジェクトを作成する
  4. 更新を返す

手順3と4は1行で組み合わせることができます。

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

React状態では、再帰的なマージは実行されませんが、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
コンポーネントでどのミックスインを使用していますか?これは私には機能しません
Sachin

-3

変更するにはtmp変数を使用します。

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} まだ問題が発生していない場合でも、これを行わないでください。
Chris
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.