useState hook setter secara tidak benar menimpa status

31

Inilah masalahnya: Saya mencoba memanggil 2 fungsi dengan mengklik tombol. Kedua fungsi memperbarui status (Saya menggunakan hook useState). Fungsi pertama memperbarui value1 dengan benar ke 'baru 1', tetapi setelah fungsi 1s (setTimeout) kedua diaktifkan, dan ia mengubah nilai 2 menjadi 'baru 2' TETAPI! Ini mengatur value1 kembali ke '1'. Mengapa ini terjadi? Terima kasih sebelumnya!

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
sumber
bisakah Anda log state pada awal changeValue2?
DanStarns
1
Saya sangat menyarankan Anda membagi objek menjadi dua panggilan terpisah useStateatau digunakan useReducer.
Jared Smith
Ya - kedua ini. Cukup gunakan dua panggilan untuk menggunakanState ()
Esben Skov Pedersen
const [state, ...], dan kemudian merujuknya di setter ... Ini akan menggunakan status yang sama sepanjang waktu.
Johannes Kuhn
Tindakan terbaik: gunakan 2 useStatepanggilan terpisah , satu untuk setiap "variabel".
Dima Tisnek

Jawaban:

30

Selamat datang di neraka penutupan . Masalah ini terjadi karena setiap kali setStatedipanggil, statemendapat referensi memori baru, tetapi fungsi changeValue1dan changeValue2, karena penutupan, tetap menggunakan statereferensi awal yang lama .

Solusi untuk memastikan setStatedari changeValue1dan changeValue2mendapatkan status terbaru adalah dengan menggunakan panggilan balik (memiliki status sebelumnya sebagai parameter):

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

  // ...
};

Anda dapat menemukan diskusi yang lebih luas tentang masalah penutupan ini di sini dan di sini .

Alberto Trindade Tavares
sumber
Panggilan balik dengan useState hook tampaknya menjadi fitur tidak berdokumen , apakah Anda yakin itu berfungsi?
HMR
@HMR Ya, itu berfungsi dan didokumentasikan di halaman lain. Lihatlah bagian "Pembaruan fungsional" di sini: reactjs.org/docs/hooks-reference.html ("Jika negara baru dihitung menggunakan keadaan sebelumnya, Anda dapat melewati fungsi untuk mengaturState")
Alberto Trindade Tavares
1
@AlbertoTrindadeTavares Ya, saya juga melihat dokumen, tidak bisa menemukan apa pun. Terima kasih banyak atas jawabannya!
Bartek
1
Solusi pertama Anda bukan hanya "solusi mudah", ini metode yang tepat. Yang kedua hanya akan berfungsi jika komponen dirancang sebagai singleton, dan itupun saya tidak yakin tentang itu karena state menjadi objek baru setiap kali.
Scimonster
1
@AlbertoTrindadeTavares terima kasih! Nice one
José Salgado
19

Fungsi Anda harus seperti ini:

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

Dengan demikian Anda memastikan Anda tidak kehilangan properti apa pun yang ada dalam kondisi saat ini dengan menggunakan status sebelumnya saat aksi dipecat. Juga dengan demikian Anda menghindari keharusan mengelola penutupan.

Dez
sumber
6

Ketika changeValue2dipanggil, keadaan awal ditahan sehingga negara kembali ke keadaan awal dan kemudian value2properti ditulis.

Lain kali changeValue2dipanggil setelah itu, ia memegang status {value1: "1", value2: "new 2"}, sehingga value1properti ditimpa.

Anda memerlukan fungsi panah untuk setStateparameter.

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
sumber
3

Apa yang terjadi adalah keduanya changeValue1dan changeValue2melihat status dari render tempat mereka dibuat , jadi ketika komponen Anda membuat untuk pertama kalinya 2 fungsi ini melihat:

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

Ketika Anda mengklik tombol, changeValue1dipanggil pertama dan ubah status menjadi {value1: "new1", value2: "2"}seperti yang diharapkan.

Sekarang, setelah 1 detik, changeValue2dipanggil, tetapi fungsi ini masih melihat status awal ( {value1; "1", value2: "2"}), jadi ketika fungsi ini memperbarui keadaan dengan cara ini:

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

Anda berakhir melihat: {value1; "1", value2: "new2"}.

sumber

El Aoutar Hamza
sumber