this.setState não está mesclando estados como eu esperaria

160

Eu tenho o seguinte estado:

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

Então eu atualizo o estado:

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

Como setStateé suposto fundir, eu esperaria que fosse:

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

Mas, em vez disso, ele come o id e o estado é:

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

Esse comportamento é esperado e qual é a solução para atualizar apenas uma propriedade de um objeto de estado aninhado?

Pickels
fonte

Respostas:

143

Eu acho setState()que não faz mesclagem recursiva.

Você pode usar o valor do estado atual this.state.selectedpara construir um novo estado e depois chamar setState()isso:

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

Eu usei a função _.extend()function (da biblioteca underscore.js) aqui para impedir modificações na selectedparte existente do estado, criando uma cópia superficial dele.

Outra solução seria escrever setStateRecursively()que mescla recursivamente um novo estado e depois chama replaceState()com ele:

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}
andreypopp
fonte
7
Isso funciona de maneira confiável se você fizer isso mais de uma vez ou se houver uma chance de reagir enfileirar as chamadas replaceState e a última vencer?
edoloughlin 24/09/14
111
Para as pessoas que preferem usar aumentar e que também estão usando babel que você pode fazer this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })que é traduzida parathis.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
Pickels
8
@Pickels isso é ótimo, bem idiomático para React e não requer underscore/ lodash. Gire em sua própria resposta, por favor.
Ericsoco 18/11/2015
14
this.state não é necessariamente atualizado . Você pode obter resultados inesperados usando o método extend. Passar uma função de atualização para setStateé a solução correta.
Strom
1
@ EdwardD'Souza É a biblioteca lodash. É geralmente chamado como sublinhado, da mesma forma que o jQuery é normalmente chamado como um sinal de dólar. (Então, é _.extend ().)
mjk
96

Os auxiliares de imutabilidade foram adicionados recentemente ao React.addons, portanto, com isso, agora você pode fazer algo como:

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

Documentação dos auxiliares de imutabilidade .

user3874611
fonte
7
atualização está obsoleta. facebook.github.io/react/docs/update.html
thedanotto 25/10
3
@thedanotto Parece que o React.addons.update()método está obsoleto, mas a biblioteca de substituição github.com/kolodny/immutability-helper contém uma update()função equivalente .
Bungle
37

Como muitas das respostas usam o estado atual como base para mesclar novos dados, eu queria ressaltar que isso pode quebrar. As alterações de estado estão na fila e não modificam imediatamente o objeto de estado de um componente. A referência aos dados do estado antes do processamento da fila fornecerá dados obsoletos que não refletem as alterações pendentes que você fez no setState. Dos documentos:

setState () não muda imediatamente this.state, mas cria uma transição de estado pendente. Acessar this.state depois de chamar esse método pode potencialmente retornar o valor existente.

Isso significa que o uso do estado "atual" como referência nas chamadas subsequentes para setState não é confiável. Por exemplo:

  1. Primeira chamada para setState, enfileirando uma alteração no objeto de estado
  2. Segunda chamada para setState. Seu estado usa objetos aninhados, então você deseja executar uma mesclagem. Antes de chamar setState, você obtém o objeto de estado atual. Este objeto não reflete as alterações na fila feitas na primeira chamada para setState, acima, porque ainda é o estado original, que agora deve ser considerado "obsoleto".
  3. Realize mesclagem. O resultado é o estado "obsoleto" original, além dos novos dados que você acabou de definir, as alterações da chamada inicial setState não são refletidas. Sua chamada setState enfileira nesta segunda alteração.
  4. Fila de processos de reação. A primeira chamada setState é processada, atualizando o estado. A segunda chamada setState é processada, atualizando o estado. O objeto do segundo setState agora substituiu o primeiro e, como os dados que você tinha ao fazer essa chamada estavam obsoletos, os dados obsoletos modificados dessa segunda chamada derrotaram as alterações feitas na primeira chamada, que foram perdidas.
  5. Quando a fila está vazia, o React determina se deve renderizar etc. Nesse ponto, você renderizará as alterações feitas na segunda chamada setState e será como se a primeira chamada setState nunca tivesse acontecido.

Se você precisar usar o estado atual (por exemplo, para mesclar dados em um objeto aninhado), setState aceitará uma função como argumento em vez de objeto; a função é chamada após qualquer atualização anterior do estado e passa o estado como argumento - portanto, isso pode ser usado para fazer alterações atômicas garantidas para respeitar as alterações anteriores.

bgannonpl
fonte
5
Existe um exemplo ou mais documentos sobre como fazer isso? afaik, ninguém leva isso em consideração.
Josef.B
@ Dennis Tente minha resposta abaixo.
Martin Dawson
Não vejo onde está o problema. O que você está descrevendo ocorreria apenas se você tentar acessar o estado "novo" depois de chamar setState. Esse é um problema completamente diferente que não tem nada a ver com a questão do OP. Acessar o estado antigo antes de chamar setState para que você possa construir um novo objeto de estado nunca seria um problema e, de fato, é exatamente o que o React espera.
nextgentech
@nextgentech "Acessar o estado antigo antes de chamar setState para que você possa construir um novo objeto de estado nunca seria um problema" - você está enganado. Estamos falando de substituir o estado com base em this.state, que pode ser obsoleto (ou melhor, atualização em fila pendente) quando você acessá-lo.
Madbreaks
1
@Madbreaks Ok, sim, ao reler os documentos oficiais, vejo que é especificado que você deve usar o formulário de função de setState para atualizar o estado de forma confiável com base no estado anterior. Dito isto, nunca encontrei um problema até o momento, se não estivesse tentando chamar setState várias vezes na mesma função. Obrigado pelas respostas que me levaram a reler as especificações.
nextgentech 31/01
10

Eu não queria instalar outra biblioteca, então aqui está outra solução.

Ao invés de:

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

Faça isso:

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

Ou, graças a @ icc97 nos comentários, ainda mais sucintamente, mas sem dúvida menos legível:

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

Além disso, para esclarecer, essa resposta não viola nenhuma das preocupações mencionadas acima pelo @bgannonpl.

Ryan Shillington
fonte
Voto a favor, verifique. Apenas me pergunto por que não fazê-lo assim? this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
Justus Romijn
1
@JustusRomijn Infelizmente, você modificaria o estado atual diretamente. Object.assign(a, b)recebe atributos b, insere / substitui-os ae depois retorna a. Reagir as pessoas ficam chateadas se você modificar o estado diretamente.
Ryan Shillington
Exatamente. Observe que isso só funciona para cópias / atribuições rasas.
precisa saber é o seguinte
1
@JustusRomijn Você pode fazer isso de acordo com MDN atribuir em uma linha se você adicionar {}como o primeiro argumento: this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });. Isso não altera o estado original.
Icc97
@ icc97 Nice! Eu adicionei isso à minha resposta.
Ryan Shillington
8

Preservando o estado anterior com base na resposta @bgannonpl:

Exemplo de Lodash :

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

Para verificar se funcionou corretamente, você pode usar o segundo retorno de chamada da função de parâmetro:

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

Eu usei mergeporque extenddescarta as outras propriedades de outra forma.

Exemplo de React Immutability :

import update from "react-addons-update";

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

Minha solução para esse tipo de situação é usar, como outra resposta apontada, os auxiliares da Imutabilidade .

Como definir o estado em profundidade é uma situação comum, criei o seguinte mixin:

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

Esse mixin está incluído na maioria dos meus componentes e geralmente não uso setStatemais diretamente.

Com este mixin, tudo o que você precisa fazer para obter o efeito desejado é chamar a função setStateinDepthda seguinte maneira:

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

Para maiores informações:

Dorian
fonte
Desculpe pela resposta tardia, mas seu exemplo do Mixin parece interessante. No entanto, se eu tiver 2 estados aninhados, por exemplo, this.state.test.one e this.state.test.two. É correto que apenas um deles seja atualizado e o outro seja excluído? Quando eu atualizar o primeiro, se o segundo i no estado, ele será removido ...
mamruoc
@mamruoc HY, this.state.test.twoainda estará lá depois que você atualizar this.state.test.oneusando setStateInDepth({test: {one: {$set: 'new-value'}}}). Eu uso esse código em todos os meus componentes para contornar a setStatefunção limitada do React .
Dorian
1
Eu encontrei a solução. Eu iniciei this.state.testcomo uma matriz. Mudando para Objetos, resolvi-o.
mamruoc
2

Estou usando classes es6 e acabei com vários objetos complexos no meu estado superior e estava tentando tornar meu componente principal mais modular. Por isso, criei um invólucro de classe simples para manter o estado no componente superior, mas permitir uma lógica mais local .

A classe wrapper assume uma função como seu construtor que define uma propriedade no estado do componente principal.

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()
    }
}

Em seguida, para cada propriedade complexa no estado superior, crio uma classe StateWrapped. Você pode definir os props padrão no construtor aqui e eles serão definidos quando a classe for inicializada; você poderá consultar o estado local para obter valores e definir o estado local, consultar funções locais e passar a cadeia:

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


}

Portanto, meu componente de nível superior precisa apenas do construtor para definir cada classe como sua propriedade de estado de nível superior, uma renderização simples e quaisquer funções que se comuniquem entre componentes.

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

Parece funcionar muito bem para meus propósitos, lembre-se de que você não pode alterar o estado das propriedades que atribui aos componentes agrupados no componente de nível superior, pois cada componente agrupado está acompanhando seu próprio estado, mas atualizando o estado no componente superior cada vez que muda.


fonte
2

A partir de agora,

Se o próximo estado depender do estado anterior, recomendamos o uso do formulário da função atualizador, em vez disso:

de acordo com a documentação https://reactjs.org/docs/react-component.html#setstate , usando:

this.setState((prevState) => {
    return {quantity: prevState.quantity + 1};
});
egidiocs
fonte
Obrigado pela citação / link, que estava faltando em outras respostas.
Madbreaks
1

Solução

Edit: Esta solução costumava usar sintaxe de propagação . O objetivo era criar um objeto sem referências prevState, para que prevStatenão fosse modificado. Mas no meu uso, prevStateàs vezes parecia ser modificado. Portanto, para uma clonagem perfeita sem efeitos colaterais, agora convertemos prevStatepara JSON e depois novamente. (A inspiração para usar o JSON veio do MDN .)

Lembrar:

Passos

  1. Faça uma cópia da propriedade a nível de raiz de stateque deseja mudança
  2. Mutar este novo objeto
  3. Crie um objeto de atualização
  4. Retornar a atualização

Os passos 3 e 4 podem ser combinados em uma linha.

Exemplo

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

Exemplo simplificado:

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

Isso segue bem as diretrizes do React. Baseado na resposta de eicksl a uma pergunta semelhante.

Tim
fonte
1

Solução ES6

Definimos o estado inicialmente

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

Estamos alterando uma propriedade em algum nível do objeto state:

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

O estado de reação não executa a mesclagem recursiva setStateenquanto espera que não haja atualizações de membro de estado no local ao mesmo tempo. Você precisa copiar objetos / matrizes fechados (com array.slice ou Object.assign) ou usar a biblioteca dedicada.

Como este. O NestedLink suporta diretamente o manuseio do estado React composto.

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

Além disso, o link para o selectedou selected.namepode ser transmitido para qualquer lugar como um único suporte e modificado com ele set.

gaperton
fonte
-2

você definiu o estado inicial?

Vou usar um pouco do meu próprio código, por exemplo:

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

Em um aplicativo em que estou trabalhando, é assim que tenho definido e usado o estado. Acredito que setStatevocê pode editar qualquer estado que desejar individualmente.

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

Lembre-se de que você deve definir o estado na React.createClassfunção que você chamougetInitialState

asherrard
fonte
1
que mixin você está usando no seu componente? Isso não funciona para mim
Sachin
-3

Eu uso o tmp var para mudar.

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}
hkongm
fonte
2
Aqui tmp.theme = v está em estado de mutação. Portanto, essa não é a maneira recomendada.
Almoraleslopez
1
let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} Não faça isso, mesmo que isso ainda não tenha lhe causado problemas.
18717 Chris