O configurador de gancho useState substitui incorretamente o estado

31

Aqui está o problema: estou tentando chamar duas funções com um clique de botão. Ambas as funções atualizam o estado (estou usando o gancho useState). A primeira função atualiza o valor1 corretamente para 'novo 1', mas após 1s (setTimeout) a segunda função é acionada e altera o valor 2 para 'novo 2' MAS! Ele define o valor1 de volta para '1'. Por que isso está acontecendo? Desde já, obrigado!

import React, { useState } from "react";

const Test = () => {
  const [state, setState] = useState({
    value1: "1",
    value2: "2"
  });

  const changeValue1 = () => {
    setState({ ...state, value1: "new 1" });
  };
  const changeValue2 = () => {
    setState({ ...state, value2: "new 2" });
  };

  return (
    <>
      <button
        onClick={() => {
          changeValue1();
          setTimeout(changeValue2, 1000);
        }}
      >
        CHANGE BOTH
      </button>
      <h1>{state.value1}</h1>
      <h1>{state.value2}</h1>
    </>
  );
};

export default Test;
Bartek
fonte
você pode registrar o estado no início de changeValue2?
DanStarns 01/10/19
11
Eu recomendo que você divida o objeto em duas chamadas separadas useStateou use useReducer.
Jared Smith
Sim - segundo isso. Basta usar duas chamadas para useState ()
Esben Skov Pedersen
const [state, ...]e, em seguida, referindo-se a ele no levantador ... Ele usará o mesmo estado o tempo todo.
Johannes Kuhn
Melhor curso de ação: use 2 useStatechamadas separadas , uma para cada "variável".
Dima Tisnek

Respostas:

30

Bem-vindo ao inferno do fechamento . Esse problema ocorre porque sempre que setStateé chamado, stateobtém uma nova referência de memória, mas as funções changeValue1e changeValue2, por causa do fechamento, mantêm a statereferência inicial antiga .

Uma solução para garantir o estado setStatede changeValue1e changeValue2obtém o estado mais recente é usando um retorno de chamada (tendo o estado anterior como parâmetro):

import React, { useState } from "react";

const Test = () => {
  const [state, setState] = useState({
    value1: "1",
    value2: "2"
  });

  const changeValue1 = () => {
    setState((prevState) => ({ ...prevState, value1: "new 1" }));
  };
  const changeValue2 = () => {
    setState((prevState) => ({ ...prevState, value2: "new 2" }));
  };

  // ...
};

Você pode encontrar uma discussão mais ampla sobre esse problema de fechamento aqui e aqui .

Alberto Trindade Tavares
fonte
Um retorno de chamada com o gancho useState parece ser um recurso não documentado . Tem certeza de que funciona?
HMR
@HMR Sim, funciona e está documentado em outra página. Dê uma olhada na seção "Atualizações funcionais" aqui: reactjs.org/docs/hooks-reference.html ("Se o novo estado for calculado usando o estado anterior, você poderá passar uma função para setState")
Alberto Trindade Tavares
11
@AlbertoTrindadeTavares Sim, eu estava olhando os documentos também, não consegui encontrar nada. Muito obrigado pela resposta!
Bartek #
11
Sua primeira solução não é apenas uma "solução fácil", é o método correto. O segundo funcionaria apenas se o componente fosse projetado como um singleton, e mesmo assim não tenho certeza disso porque o estado se torna um novo objeto a cada vez.
Scimonster 02/10/19
11
Obrigado @AlbertoTrindadeTavares! Nice one
José Salgado
19

Suas funções devem ser assim:

const changeValue1 = () => {
    setState((prevState) => ({ ...prevState, value1: "new 1" }));
};
const changeValue2 = () => {
    setState((prevState) => ({ ...prevState, value2: "new 2" }));
};

Portanto, verifique se não está faltando nenhuma propriedade existente no estado atual usando o estado anterior quando a ação é acionada. Além disso, você evita ter que gerenciar fechamentos.

Dez
fonte
6

Quando changeValue2é invocado, o estado inicial é mantido para que o estado volte ao estado inicial e, em seguida, a value2propriedade é gravada.

A próxima vez que changeValue2for invocada depois disso, ele mantém o estado {value1: "1", value2: "new 2"}, portanto, a value1propriedade é substituída.

Você precisa de uma função de seta para o setStateparâmetro.

const Test = () => {
  const [state, setState] = React.useState({
    value1: "1",
    value2: "2"
  });

  const changeValue1 = () => {
    setState(prev => ({ ...prev, value1: "new 1" }));
  };
  const changeValue2 = () => {
    setState(prev => ({ ...prev, value2: "new 2" }));
  };

  return (
    <React.Fragment>
      <button
        onClick={() => {
          changeValue1();
          setTimeout(changeValue2, 1000);
        }}
      >
        CHANGE BOTH
      </button>
      <h1>{state.value1}</h1>
      <h1>{state.value2}</h1>
    </React.Fragment>
  );
};

ReactDOM.render(<Test />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

zmag
fonte
3

O que está acontecendo é que ambos changeValue1e changeValue2ver o estado do renderizar eles foram criados em , por isso, quando o componente de tornar pela primeira vez estes 2 funções, consulte:

state= {
  value1: "1",
  value2: "2"
}

Quando você clica no botão, changeValue1é chamado primeiro e altera o estado para {value1: "new1", value2: "2"}o esperado.

Agora, após 1 segundo, changeValue2é chamado, mas essa função ainda vê o estado inicial ( {value1; "1", value2: "2"}), portanto, quando essa função atualiza o estado da seguinte maneira:

setState({ ...state, value2: "new 2" });

você acaba vendo: {value1; "1", value2: "new2"}.

fonte

El Aoutar Hamza
fonte