seter haka useState niepoprawnie nadpisuje stan

31

Oto problem: próbuję wywołać 2 funkcje jednym kliknięciem przycisku. Obie funkcje aktualizują stan (używam haka useState). Pierwsza funkcja poprawnie aktualizuje wartość 1 do „nowej 1”, ale po 1 s (setTimeout) uruchamia się druga funkcja i zmienia wartość 2 na „nową 2” ALE! Ustawia wartość 1 z powrotem na „1”. Dlaczego to się dzieje? Z góry dziękuję!

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
źródło
czy możesz zalogować stan na początku changeValue2?
DanStarns
1
Zdecydowanie zalecamy podzielenie obiektu na dwa osobne wywołania useStatelub użycie useReducer.
Jared Smith
Tak - po drugie. Wystarczy użyć dwóch wywołań funkcji useState ()
Esben Skov Pedersen
const [state, ...], a następnie odnosząc się do niego w seterie ... Cały czas będzie używać tego samego stanu.
Johannes Kuhn
Najlepszy sposób działania: użyj 2 oddzielnych useStatewywołań, po jednym dla każdej „zmiennej”.
Dima Tisnek

Odpowiedzi:

30

Witamy w zamknięciu piekła . Ten problem występuje, ponieważ po każdym setStatewywołaniu stateotrzymuje nowe odwołanie do pamięci, ale funkcje changeValue1i changeValue2, z powodu zamknięcia, zachowują stare początkowe stateodwołanie.

Rozwiązaniem zapewniającym setStateod changeValue1i changeValue2uzyskanie najnowszego stanu jest użycie wywołania zwrotnego (posiadającego poprzedni stan jako parametr):

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

  // ...
};

Szerszą dyskusję na temat tego problemu zamknięcia można znaleźć tutaj i tutaj .

Alberto Trindade Tavares
źródło
Oddzwonienie przy użyciu haka useState wydaje się być nieudokumentowaną funkcją, czy na pewno działa?
HMR
@HMR Tak, działa i jest udokumentowane na innej stronie. Spójrz na sekcję „Aktualizacje funkcjonalne” tutaj: reagjs.org/docs/hooks-reference.html („Jeśli nowy stan jest obliczany przy użyciu poprzedniego stanu, możesz przekazać funkcję do setState”)
Alberto Trindade Tavares
1
@AlbertoTrindadeTavares Tak, również patrzyłem na dokumenty, nic nie mogłem znaleźć. Wielkie dzięki za odpowiedź!
Bartek
1
Twoje pierwsze rozwiązanie to nie tylko „łatwe rozwiązanie”, to właściwa metoda. Drugi działałby tylko wtedy, gdyby komponent został zaprojektowany jako singleton i nawet wtedy nie jestem tego pewien, ponieważ stan za każdym razem staje się nowym obiektem.
Scimonster,
1
Dziękuję @AlbertoTrindadeTavares! Niezły
José Salgado
19

Twoje funkcje powinny wyglądać tak:

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

Dzięki temu upewnisz się, że nie brakuje żadnej istniejącej właściwości w bieżącym stanie, używając poprzedniego stanu po uruchomieniu akcji. W ten sposób unikasz również zarządzania zamknięciami.

Dez
źródło
6

Po changeValue2wywołaniu stan początkowy jest utrzymywany, więc stan wraca do stanu początkowego, a następnie value2zapisywana jest właściwość.

Następnym razem changeValue2wywoływany jest stan {value1: "1", value2: "new 2"}, więc value1właściwość zostaje nadpisana.

Potrzebujesz setStateparametru strzałki dla parametru.

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
źródło
3

Co się dzieje jest to, że zarówno changeValue1i changeValue2zobaczyć stan z tynku zostały stworzone w , więc gdy składnik uczynić po raz pierwszy te 2 funkcje patrz:

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

Po kliknięciu przycisku changeValue1wywoływany jest pierwszy i zmienia stan {value1: "new1", value2: "2"}zgodnie z oczekiwaniami.

Teraz, po 1 sekundzie, changeValue2jest wywoływana, ale ta funkcja nadal widzi stan początkowy ( {value1; "1", value2: "2"}), więc kiedy ta funkcja aktualizuje stan w ten sposób:

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

skończyć się zobaczyć: {value1; "1", value2: "new2"}.

źródło

El Aoutar Hamza
źródło