web-dev-qa-db-ja.com

Reactで入れ子になった状態プロパティを更新する方法

このようにネストしたプロパティを使って自分の状態を整理しようとしています。

this.state = {
   someProperty: {
      flag:true
   }
}

しかし、このように状態を更新すると、

this.setState({ someProperty.flag: false });

うまくいきません。どうすればこれを正しくできますか?

208
Alex Yong

入れ子になったオブジェクトに対してsetStateを実行するには、setStateは入れ子になった更新を処理しないと思うので、以下のアプローチに従うことができます。

var someProperty = {...this.state.someProperty}
someProperty.flag = true;
this.setState({someProperty})

これは、ダミーオブジェクトを作成してダミーオブジェクトを操作し、コンポーネントの状態を更新されたオブジェクトに置き換えることです。

現在、スプレッド演算子は、オブジェクトの1レベルのネストされたコピーのみを作成します。あなたの状態が次のように高度にネストされているとします。

this.state = {
   someProperty: {
      someOtherProperty: {
          anotherProperty: {
             flag: true
          }
          ..
      }
      ...
   }
   ...
}

次のように各レベルでスプレッド演算子を使用してsetStateを設定できます。

this.setState(prevState => ({
    ...prevState,
    someProperty: {
        ...prevState.someProperty,
        someOtherProperty: {
            ...prevState.someProperty.someOtherProperty, 
            anotherProperty: {
               ...prevState.someProperty.someOtherProperty.anotherProperty,
               flag: false
            }
        }
    }
}))

しかし、上記の構文は、状態がますますネストされるにつれて醜くなるので、状態を更新するには immutability-helper packageを使用することをお勧めします。

immutability helper で状態を更新する方法については、この回答を参照してください。

254
Shubham Khatri

一行で書く

this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
91
Yoseph

直接の答えが最善ではないこともあります:)

短縮版:

このコード

this.state = {
    someProperty: {
        flag: true
    }
}

のように単純化する必要があります

this.state = {
    somePropertyFlag: true
}

ロングバージョン:

現在 あなたはReactで入れ子になった状態で作業したくないはずです 。 Reactは入れ子になった状態で動作するように指向されておらず、ここで提案されているすべてのソリューションはハックのように見えます。彼らはフレームワークを使わずにそれと戦っています。彼らは、いくつかの特性をグループ化するという疑わしい目的のために、それほど明確ではないコードを書くことを提案しています。それで彼らは挑戦への答えとして非常に面白いが実用的には役に立たない。

次のような状態を想像してみましょう。

{
    parent: {
        child1: 'value 1',
        child2: 'value 2',
        ...
        child100: 'value 100'
    }
}

child1の値だけを変更するとどうなりますか? Reactは浅い比較を使用しているためビューを再レンダリングしません。また、parentプロパティは変更されていません。ところで、状態オブジェクトを直接変更することは、一般的に悪い方法と考えられています。

そのため、parentオブジェクト全体を作り直す必要があります。しかしこの場合、私たちは別の問題に出会うでしょう。 Reactはすべての子供たちが彼らの価値観を変えたと思い、それらすべてを再レンダリングするでしょう。もちろんパフォーマンスには向いていません。

shouldComponentUpdate()でいくつかの複雑なロジックを書くことによってその問題を解決することはまだ可能ですが、私はここでやめて、短いバージョンから簡単な解決策を使うのを好むでしょう。

51

免責事項

Reactの入れ子状態は間違ったデザインです

読んで このすばらしい答え

この答えの背後にある推論:

ReactのsetStateは単なる組み込みの便利さですが、すぐに限界があることに気付くでしょう。カスタムプロパティとforceUpdateのインテリジェントな使用を使用すると、はるかに多くのことができます。例えば:

class MyClass extends React.Component {
    myState = someObject
    inputValue = 42
...

たとえば、MobXは状態を完全に切り離し、カスタムの観察可能なプロパティを使用します。
Reactコンポーネントでは状態の代わりにObservablesを使用してください。


あなたの惨めさへの答え - ここの例を見てください

ネストしたプロパティを更新するための別の短いの方法があります。

this.setState(state => {
  state.nested.flag = false
  state.another.deep.prop = true
  return state
})

一行で

this.setState(state => (state.nested.flag = false, state))

これは事実上同じです

this.state.nested.flag = false
this.forceUpdate()

この文脈におけるforceUpdatesetStateの微妙な違いについては、リンクされた例を見てください。

stateは読み取り専用でなければならないので、もちろんこれはいくつかのコア原則を悪用していますが、すぐに古い状態を破棄して新しい状態に置き換えるので、まったく問題ありません。

警告

状態{willを含むコンポーネントが正しく更新され、正しくレンダリングされた場合でも、{これ以外は _になりますが、小道具はfailで子に伝播します(下記のSpymasterのコメント参照)。自分がしていることがわかっている場合にのみ、この手法を使用してください。

たとえば、更新されて簡単に渡される、変更されたフラットプロップを渡すことができます。

render(
  //some complex render with your nested state
  <ChildComponent complexNestedProp={this.state.nested} pleaseRerender={Math.random()}/>
)

ComplexNestedPropの参照は変更されませんでしたが( shouldComponentUpdate

this.props.complexNestedProp === nextProps.complexNestedProp

コンポーネントwillは、親コンポーネントが更新されるたびに再描画されます。これは、親でthis.setStateまたはthis.forceUpdateを呼び出した後のケースです。

状態を変更した場合の影響

ネストしたstate を使用して状態を直接変更するのは危険です。なぜなら、異なるオブジェクトが(意図的かどうかにかかわらず) state への異なる(古い)参照を保持し、 PureComponentを使用する場合、またはshouldComponentUpdatefalseを返すように実装されている場合の例) _または_ は、以下の例のように古いデータを表示するためのものです。

履歴データをレンダリングすることになっているタイムラインを想像してください。手元のデータを変更すると、前の項目も変更されるため、予期しない動作が発生します。

state-flow state-flow-nested

とにかくここであなたはNested PureChildClassが小道具が伝播できなかったためにレンダリングされていないことがわかります。

37
Qwerty

ES2015を使用している場合は、Object.assignにアクセスできます。次のように使用して、入れ子になったオブジェクトを更新できます。

this.setState({
  someProperty: Object.assign({}, this.state.someProperty, {flag: false})
});

更新したプロパティを既存のプロパティとマージし、返されたオブジェクトを使用して状態を更新します。

編集:carkodが指摘したように、状態が直接変化しないようにするために、割り当て関数に空のオブジェクトをターゲットとして追加.

20
Alyssa Roose

これを手助けするライブラリはたくさんあります。たとえば、 immutability-helper を使用します。

import update from 'immutability-helper';

const newState = update(this.state, {
  someProperty: {flag: {$set: false}},
};
this.setState(newState);

lodash/fp setを使用します。

import {set} from 'lodash/fp';

const newState = set(["someProperty", "flag"], false, this.state);

lodash/fp mergeを使用します。

import {merge} from 'lodash/fp';

const newState = merge(this.state, {
  someProperty: {flag: false},
});
12
tokland

ES6の場合:

this.setState({...this.state, property: {nestedProperty: "new value"}})

これは以下と同等です。

const newState = Object.assign({}, this.state);
newState.property.nestedProperty = "new value";
this.setState(newState);
10
enzolito

追加のパッケージ、ライブラリ、特別な関数を必要としない、このスレッドで与えられた最初の答えのバリエーションです。

state = {
  someProperty: {
    flag: 'string'
  }
}

handleChange = (value) => {
  const newState = {...this.state.someProperty, flag: value}
  this.setState({ someProperty: newState })
}

特定のネストしたフィールドの状態を設定するために、オブジェクト全体を設定しました。これを行うには、変数newStateを作成し、ES2015 spread演算子 を使用して現在の状態の内容をfirstに展開します。その後、this.state.flagの値を新しい値に置き換えました(flag: valueafterを設定したため、現在の状態をオブジェクトに展開したので、現在の状態のflagフィールドは上書きされます)。それから、somePropertyの状態を自分のnewStateオブジェクトに設定します。

5

このような問題に対処するには、Immer https://github.com/mweststrate/immer を使用します。

このコードをコンポーネントの1つに置き換えただけです。

this.setState(prevState => ({
   ...prevState,
        preferences: {
            ...prevState.preferences,
            [key]: newValue
        }
}));

これとともに

import produce from 'immer';

this.setState(produce(draft => {
    draft.preferences[key] = newValue;
}));

水浸しであなたはあなたの状態を「普通のオブジェクト」として扱う。マジックはプロキシオブジェクトを使って舞台裏で起こります。

5

私はこの解決策を使いました。

あなたがこのような入れ子になった状態があるならば:

   this.state = {
          formInputs:{
            friendName:{
              value:'',
              isValid:false,
              errorMsg:''
            },
            friendEmail:{
              value:'',
              isValid:false,
              errorMsg:''
            }
}

現在のステータスをコピーして値を変更して再割り当てするhandleChange関数を宣言できます。

handleChange(el) {
    let inputName = el.target.name;
    let inputValue = el.target.value;

    let statusCopy = Object.assign({}, this.state);
    statusCopy.formInputs[inputName].value = inputValue;

    this.setState(statusCopy);
  }

ここでイベントリスナーを含むHTML

<input type="text" onChange={this.handleChange} " name="friendName" />
3
Alberto Piras

私はあなたのコンポーネントの状態の完全なコピーを作成することをめぐる懸念{ すでに表明されている を真剣に考えています。それでも、Immerを強くお勧めします。

import produce from 'immer';

<Input
  value={this.state.form.username}
  onChange={e => produce(this.state, s => { s.form.username = e.target.value }) } />

Immerは巧妙に任意の深い状態ツリーを効率的にコピーするためにプロキシオブジェクトを使用するので、これはReact.PureComponent(すなわちReactによる浅い状態比較)のために働くべきです。 ImmerはImmutability Helperのようなライブラリに比べて型保証も多く、JavascriptユーザーとTypeScriptユーザーの両方にとって理想的です。


TypeScript ユーティリティ関数

function setStateDeep<S>(comp: React.Component<any, S, any>, fn: (s: 
Draft<Readonly<S>>) => any) {
  comp.setState(produce(comp.state, s => { fn(s); }))
}

onChange={e => setStateDeep(this, s => s.form.username = e.target.value)}
2
Stephen Paul

まだ言及されていない他の2つのオプション:

  1. 深くネストされた状態がある場合は、子オブジェクトを再構築してルートに座ることができるかどうかを検討してください。これによりデータの更新が容易になります。
  2. 不変の状態を処理するために利用できる便利なライブラリがたくさんあります Reduxのドキュメントにリストされています 。私はImmerをお勧めします。それはあなたがコードを変更可能な方法で書くことを可能にしますが、舞台裏で必要な複製を処理するからです。結果のオブジェクトもフリーズするので、後で誤って変更することはありません。
2
Cory House

状態のコピーを作成します。
let someProperty = JSON.parse(JSON.stringify(this.state.someProperty))

このオブジェクトを変更します。
someProperty.flag = "false"

今状態を更新します
this.setState({someProperty})

2
Dhakad

入れ子にすることは実際にはコンポーネントの状態をどのように扱うべきかということではありませんが、単層の入れ子にするのが簡単なこともあります。

こんな状態に

state = {
 contact: {
  phone: '888-888-8888',
  email: '[email protected]'
 }
 address: {
  street:''
 },
 occupation: {
 }
}

使用できる再利用可能なメソッドはこのようになります。

handleChange = (obj) => e => {
  let x = this.state[obj];
  x[e.target.name] = e.target.value;
  this.setState({ [obj]: x });
};

それからあなたがアドレスしたいそれぞれのネスティングのためのobj名を渡すだけで...

<TextField
 name="street"
 onChange={handleChange('address')}
 />
1
user2208124

物事を一般的なものにするために、私は@ ShubhamKhatriと@ Qwertyの答えに取り組みました。

状態オブジェクト

this.state = {
  name: '',
  grandParent: {
    parent1: {
      child: ''
    },
    parent2: {
      child: ''
    }
  }
};

入力コントロール

<input
  value={this.state.name}
  onChange={this.updateState}
  type="text"
  name="name"
/>
<input
  value={this.state.grandParent.parent1.child}
  onChange={this.updateState}
  type="text"
  name="grandParent.parent1.child"
/>
<input
  value={this.state.grandParent.parent2.child}
  onChange={this.updateState}
  type="text"
  name="grandParent.parent2.child"
/>

updateStateメソッド

@ ShubhamKhatriの答えとしてsetState

updateState(event) {
  const path = event.target.name.split('.');
  const depth = path.length;
  const oldstate = this.state;
  const newstate = { ...oldstate };
  let newStateLevel = newstate;
  let oldStateLevel = oldstate;

  for (let i = 0; i < depth; i += 1) {
    if (i === depth - 1) {
      newStateLevel[path[i]] = event.target.value;
    } else {
      newStateLevel[path[i]] = { ...oldStateLevel[path[i]] };
      oldStateLevel = oldStateLevel[path[i]];
      newStateLevel = newStateLevel[path[i]];
    }
  }
  this.setState(newstate);
}

@ Qwertyの回答としてsetState

updateState(event) {
  const path = event.target.name.split('.');
  const depth = path.length;
  const state = { ...this.state };
  let ref = state;
  for (let i = 0; i < depth; i += 1) {
    if (i === depth - 1) {
      ref[path[i]] = event.target.value;
    } else {
      ref = ref[path[i]];
    }
  }
  this.setState(state);
}

注意:上記のメソッドは配列には使えません

0
Venugopal

私はそれが古い質問であることを知っていますが、それでも私がこれを達成した方法を共有したいと思いました。コンストラクタ内の状態を仮定すると、次のようになります。

  constructor(props) {
    super(props);

    this.state = {
      loading: false,
      user: {
        email: ""
      },
      organization: {
        name: ""
      }
    };

    this.handleChange = this.handleChange.bind(this);
  }

私のhandleChange関数はこんな感じです:

  handleChange(e) {
    const names = e.target.name.split(".");
    const value = e.target.type === "checkbox" ? e.target.checked : e.target.value;
    this.setState((state) => {
      state[names[0]][names[1]] = value;
      return {[names[0]]: state[names[0]]};
    });
  }

それに応じて入力に名前を付けます。

<input
   type="text"
   name="user.email"
   onChange={this.handleChange}
   value={this.state.user.firstName}
   placeholder="Email Address"
/>

<input
   type="text"
   name="organization.name"
   onChange={this.handleChange}
   value={this.state.organization.name}
   placeholder="Organization Name"
/>
0
Elnoor

私はこれがうまくいくことがわかりました。私の場合はプロジェクトフォームを用意しました。たとえば、あなたはIDと名前を持っていました。ネストしたプロジェクトの状態を維持したいのです。

return (
  <div>
      <h2>Project Details</h2>
      <form>
        <Input label="ID" group type="number" value={this.state.project.id} onChange={(event) => this.setState({ project: {...this.state.project, id: event.target.value}})} />
        <Input label="Name" group type="text" value={this.state.project.name} onChange={(event) => this.setState({ project: {...this.state.project, name: event.target.value}})} />
      </form> 
  </div>
)

お知らせ下さい!

0
Michael Stokes

reduce検索でネストされた更新を行います:

例:

状態のネストされた変数:

state = {
    coords: {
        x: 0,
        y: 0,
        z: 0
    }
}

関数:

handleChange = nestedAttr => event => {
  const { target: { value } } = event;
  const attrs = nestedAttr.split('.');

  let stateVar = this.state[attrs[0]];
  if(attrs.length>1)
    attrs.reduce((a,b,index,arr)=>{
      if(index==arr.length-1)
        a[b] = value;
      else if(a[b]!=null)
        return a[b]
      else
        return a;
    },stateVar);
  else
    stateVar = value;

  this.setState({[attrs[0]]: stateVar})
}

使用:

<input
value={this.state.coords.x}
onChange={this.handleTextChange('coords.x')}
/>
0
Emisael Carrera

このような何かで十分かもしれません、

const isObject = (thing) => {
    if(thing && 
        typeof thing === 'object' &&
        typeof thing !== null
        && !(Array.isArray(thing))
    ){
        return true;
    }
    return false;
}

/*
  Call with an array containing the path to the property you want to access
  And the current component/redux state.

  For example if we want to update `hello` within the following obj
  const obj = {
     somePrimitive:false,
     someNestedObj:{
        hello:1
     }
  }

  we would do :
  //clone the object
  const cloned = clone(['someNestedObj','hello'],obj)
  //Set the new value
  cloned.someNestedObj.hello = 5;

*/
const clone = (arr, state) => {
    let clonedObj = {...state}
    const originalObj = clonedObj;
    arr.forEach(property => {
        if(!(property in clonedObj)){
            throw new Error('State missing property')
        }

        if(isObject(clonedObj[property])){
            clonedObj[property] = {...originalObj[property]};
            clonedObj = clonedObj[property];
        }
    })
    return originalObj;
}

const nestedObj = {
    someProperty:true,
    someNestedObj:{
        someOtherProperty:true
    }
}

const clonedObj = clone(['someProperty'], nestedObj);
console.log(clonedObj === nestedObj) //returns false
console.log(clonedObj.someProperty === nestedObj.someProperty) //returns true
console.log(clonedObj.someNestedObj === nestedObj.someNestedObj) //returns true

console.log()
const clonedObj2 = clone(['someProperty','someNestedObj','someOtherProperty'], nestedObj);
console.log(clonedObj2 === nestedObj) // returns false
console.log(clonedObj2.someNestedObj === nestedObj.someNestedObj) //returns false
//returns true (doesn't attempt to clone because its primitive type)
console.log(clonedObj2.someNestedObj.someOtherProperty === nestedObj.someNestedObj.someOtherProperty) 
0
Eladian
stateUpdate = () => {
    let obj = this.state;
    if(this.props.v12_data.values.email) {
      obj.obj_v12.Customer.EmailAddress = this.props.v12_data.values.email
    }
    this.setState(obj)
}
0
user3444748