web-dev-qa-db-ja.com

this.setStateは私が期待するように状態をマージしていません

私の状態は次のとおりです。

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

次に、状態を更新します。

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

SetStateはマージすることを前提としているため、次のようになります。

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

しかし、代わりにidを食べ、状態は次のとおりです。

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

これは予想される動作ですか?ネストされた状態オブジェクトの1つのプロパティのみを更新するソリューションは何ですか?

148
Pickels

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);
}
137
andreypopp

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

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

不変性ヘルパーのドキュメント: http://facebook.github.io/react/docs/update.html

95
user3874611

回答の多くは現在の状態を新しいデータのマージの基礎として使用しているため、これが壊れる可能性があることを指摘したいと思いました。状態の変更はキューに入れられ、コンポーネントの状態オブジェクトをすぐには変更しません。したがって、キューが処理される前に状態データを参照すると、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のオブジェクトは1番目のオブジェクトに置き換わり、その呼び出しを行ったときに持っていたデータが古いため、この2番目の呼び出しから変更された古いデータは、最初の呼び出しで行われた変更を失わせます。
  5. キューが空の場合、Reactはレンダリングなどを行うかどうかを決定します。この時点で、2番目のsetState呼び出しで行われた変更をレンダリングし、最初のsetState呼び出しは発生しなかったようになります。

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

33
bgannonpl

別のライブラリをインストールしたくなかったので、別のソリューションを紹介します。

の代わりに:

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が懸念することのいずれにも違反していません。

10

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

Lodash例:

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

適切に動作することを確認するには、2番目のパラメーター関数コールバックを使用できます。

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

mergeは他のプロパティを破棄するため、extendを使用しました。

React Immutability例:

import update from "react-addons-update";

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

この種の状況に対する私の解決策は、指摘された別の答えのように、 不変性ヘルパー を使用することです。

状態を詳細に設定することは一般的な状況であるため、次のミックスインを作成しました。

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

このミックスインはほとんどのコンポーネントに含まれており、通常はsetStateを直接使用しなくなりました。

このミックスインを使用して、目的の効果を得るために必要なことは、次の方法で関数setStateinDepthを呼び出すことだけです。

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

詳細については:

  • Reactでのmixinの動作については、 公式ドキュメント をご覧ください
  • setStateinDepthに渡されるパラメーターの構文については、Immutability Helpers documentation を参照してください。
2
Dorian

今のところ、

次の状態が前の状態に依存している場合は、代わりにアップデーター関数フォームを使用することをお勧めします。

ドキュメントによると https://reactjs.org/docs/react-component.html#setstate 、以下を使用:

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

溶液

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

覚えておいてください:

手順

  1. 変更するstaterootレベルのプロパティのコピーを作成します
  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's に基づく同様の質問への回答。

1
Tim

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' } }
1
Roman

私は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
user1641172

React状態は、setStateで再帰的マージを実行しませんが、インプレース状態メンバーの更新が同時に行われないことを期待しています。囲まれたオブジェクト/配列を自分で(array.sliceまたはObject.assignで)コピーするか、専用ライブラリを使用する必要があります。

このように。 NestedLink は、複合React状態の処理を直接サポートします。

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

また、selectedまたはselected.nameへのリンクは、単一のプロップとしてどこにでも渡すことができ、そこでsetで変更できます。

0
gaperton