metode set useState tidak mencerminkan perubahan segera

167

Saya mencoba mempelajari kait dan useStatemetode ini membuat saya bingung. Saya menetapkan nilai awal ke keadaan dalam bentuk array. Metode yang diset di useStatetidak berfungsi untuk saya bahkan dengan spread(...)atau without spread operator. Saya telah membuat API di PC lain yang saya panggil dan mengambil data yang ingin saya setel ke status.

Ini kode saya:

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const StateSelector = () => {
  const initialValue = [
    {
      category: "",
      photo: "",
      description: "",
      id: 0,
      name: "",
      rating: 0
    }
  ];

  const [movies, setMovies] = useState(initialValue);

  useEffect(() => {
    (async function() {
      try {
        //const response = await fetch(
        //`http://192.168.1.164:5000/movies/display`
        //);
        //const json = await response.json();
        //const result = json.data.result;
        const result = [
          {
            category: "cat1",
            description: "desc1",
            id: "1546514491119",
            name: "randomname2",
            photo: null,
            rating: "3"
          },
          {
            category: "cat2",
            description: "desc1",
            id: "1546837819818",
            name: "randomname1",
            rating: "5"
          }
        ];
        console.log(result);
        setMovies(result);
        console.log(movies);
      } catch (e) {
        console.error(e);
      }
    })();
  }, []);

  return <p>hello</p>;
};

const rootElement = document.getElementById("root");
ReactDOM.render(<StateSelector />, rootElement);

The setMovies(result)serta setMovies(...result)tidak bekerja. Bisa menggunakan bantuan di sini.

Saya berharap variabel hasil didorong ke array film.

Pranjal
sumber

Jawaban:

219

Sama seperti komponen setState di Kelas yang dibuat dengan cara memperpanjang React.Componentatau React.PureComponent, pembaruan status menggunakan pembaru yang disediakan oleh useStatehook juga tidak sinkron, dan tidak akan langsung mencerminkan

Juga masalah utama di sini tidak hanya sifat asinkron tetapi fakta bahwa nilai-nilai negara digunakan oleh fungsi berdasarkan penutupan mereka saat ini dan pembaruan keadaan akan tercermin dalam render ulang berikutnya dengan mana penutupan yang ada tidak terpengaruh tetapi yang baru dibuat . Sekarang dalam keadaan saat ini nilai-nilai dalam kait diperoleh dengan penutupan yang ada dan ketika terjadi render, penutupan diperbarui berdasarkan pada apakah fungsi diciptakan kembali atau tidak.

Sekalipun Anda menambahkan setTimeoutfungsi, meskipun batas waktu akan berjalan setelah beberapa waktu yang akan menyebabkan render ulang, tetapi setTimout masih akan menggunakan nilai dari penutupan sebelumnya dan bukan yang diperbarui.

setMovies(result);
console.log(movies) // movies here will not be updated

Jika Anda ingin melakukan tindakan pada pembaruan negara, Anda perlu menggunakan hook useEffect, seperti menggunakan componentDidUpdatekomponen di kelas karena setter yang dikembalikan oleh useState tidak memiliki pola panggilan balik

useEffect(() => {
    // action on update of movies
}, [movies]);

Sejauh menyangkut sintaks untuk memperbarui status, setMovies(result)akan mengganti nilai sebelumnya moviesdalam status dengan yang tersedia dari permintaan async

Namun jika Anda ingin menggabungkan respons dengan nilai-nilai yang sudah ada sebelumnya, Anda harus menggunakan sintaks callback pembaruan status bersama dengan penggunaan yang benar dari sintaksis penyebaran seperti

setMovies(prevMovies => ([...prevMovies, ...result]));
Shubham Khatri
sumber
17
Hai, bagaimana dengan memanggil useState di dalam form submit handler? Saya sedang bekerja untuk memvalidasi formulir yang kompleks, dan saya memanggil di dalam submitHandler useState hooks dan sayangnya perubahannya tidak langsung terjadi!
Da45
2
useEffectmungkin bukan solusi terbaik, karena tidak mendukung panggilan tidak sinkron. Jadi, jika kami ingin membuat validasi asinkron pada moviesperubahan status, kami tidak memiliki kendali atasnya.
RA.
2
harap dicatat bahwa meskipun sarannya sangat bagus, penjelasan penyebabnya dapat ditingkatkan - tidak ada hubungannya dengan fakta apakah the updater provided by useState hook asinkron atau tidak , tidak seperti this.stateyang bisa dimutasi jika this.setStatesinkron, Penutupan const moviesakan tetap sama bahkan jika useStatemenyediakan fungsi sinkron - lihat contoh dalam jawaban saya
Aprillion
setMovies(prevMovies => ([...prevMovies, ...result]));bekerja untuk saya
Mihir
Ini mencatat hasil yang salah karena Anda mencatat penutupan basi bukan karena setter tidak sinkron. Jika async adalah masalah maka Anda bisa login setelah waktu habis, tetapi Anda bisa menetapkan batas waktu selama satu jam dan tetap mencatat hasil yang salah karena async bukan yang menyebabkan masalah.
HMR
126

Rincian tambahan untuk jawaban sebelumnya :

Sementara Bereaksi ini setState adalah asynchronous (kedua kelas dan kait), dan itu tergoda untuk menggunakan fakta bahwa untuk menjelaskan perilaku yang diamati, itu bukan alasan mengapa hal itu terjadi.

TLDR: Alasannya adalah cakupan penutupan di sekitar constnilai yang tidak berubah .


Solusi:

  • baca nilai dalam fungsi render (tidak di dalam fungsi bersarang):

    useEffect(() => { setMovies(result) }, [])
    console.log(movies)
  • tambahkan variabel ke dependensi (dan gunakan aturan eslint react-hooks / exhaustive-deps ):

    useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
  • gunakan referensi yang bisa berubah-ubah (bila hal di atas tidak mungkin):

    const moviesRef = useRef(initialValue)
    useEffect(() => {
      moviesRef.current = result
      console.log(moviesRef.current)
    }, [])

Penjelasan mengapa itu terjadi:

Jika async adalah satu-satunya alasan, itu mungkin await setState() .

Howerver, baik propsdan stateyang diasumsikan tidak berubah selama 1 render .

Perlakukan this.stateseolah-olah itu abadi.

Dengan kait, asumsi ini ditingkatkan dengan menggunakan nilai konstan dengan constkata kunci:

const [state, setState] = useState('initial')

Nilai mungkin berbeda antara 2 render, tetapi tetap konstan di dalam render itu sendiri dan di dalam setiap penutupan (fungsi yang hidup lebih lama bahkan setelah render selesai, misalnyauseEffect , penangan acara, di dalam Janji atau setTimeout apa pun).

Pertimbangkan untuk mengikuti implementasi React-like palsu, tetapi sinkron , seperti:

// sync implementation:

let internalState
let renderAgain

const setState = (updateFn) => {
  internalState = updateFn(internalState)
  renderAgain()
}

const useState = (defaultState) => {
  if (!internalState) {
    internalState = defaultState
  }
  return [internalState, setState]
}

const render = (component, node) => {
  const {html, handleClick} = component()
  node.innerHTML = html
  renderAgain = () => render(component, node)
  return handleClick
}

// test:

const MyComponent = () => {
  const [x, setX] = useState(1)
  console.log('in render:', x) // ✅
  
  const handleClick = () => {
    setX(current => current + 1)
    console.log('in handler/effect/Promise/setTimeout:', x) // ❌ NOT updated
  }
  
  return {
    html: `<button>${x}</button>`,
    handleClick
  }
}

const triggerClick = render(MyComponent, document.getElementById('root'))
triggerClick()
triggerClick()
triggerClick()
<div id="root"></div>

Aprillion
sumber
11
Jawaban yang sangat bagus. IMO, jawaban ini akan menyelamatkan Anda dari kesedihan terbesar di masa depan jika Anda menginvestasikan cukup waktu untuk membaca dan menyerapnya.
Scott Mallon
Ini membuatnya sangat sulit untuk menggunakan konteks dalam hubungannya dengan router, ya? Ketika saya sampai ke halaman berikutnya keadaan hilang .. Dan saya tidak dapat mengaturState hanya di dalam useEffect hooks karena mereka tidak dapat membuat ... Saya menghargai kelengkapan jawabannya dan itu menjelaskan apa yang saya lihat, tapi saya tidak tahu bagaimana cara mengalahkannya. Saya melewati fungsi dalam konteks untuk memperbarui nilai yang kemudian tidak bertahan secara efektif ... Jadi saya harus mengeluarkannya dari penutupan dalam konteks, ya?
Al Joslin
@AlJoslin pada pandangan pertama, itu tampak seperti masalah yang terpisah, bahkan jika itu mungkin disebabkan oleh lingkup penutupan. Jika Anda memiliki pertanyaan konkret, harap buat pertanyaan stackoverflow baru dengan contoh kode dan semuanya ...
Aprillion
1
sebenarnya saya baru saja selesai menulis ulang dengan useReducer, mengikuti artikel @kentcdobs (ref di bawah) yang benar-benar memberi saya hasil yang solid yang tidak menderita sedikit pun dari masalah penutupan ini. (ref: kentcdodds.com/blog/how-to-use-react-context- secara efektif )
Al Joslin
1

Saya baru saja selesai menulis ulang dengan useReducer, mengikuti artikel @kentcdobs (ref di bawah) yang benar-benar memberi saya hasil yang solid yang tidak menderita sedikit pun dari masalah penutupan ini.

lihat: https://kentcdodds.com/blog/how-to-use-react-context-effectively

Saya memadatkan boilerplate-nya yang dapat dibaca ke tingkat DRYness yang saya sukai - membaca implementasi sandbox-nya akan menunjukkan kepada Anda bagaimana cara kerjanya.

Selamat menikmati, saya tahu saya !!

import React from 'react'

// ref: https://kentcdodds.com/blog/how-to-use-react-context-effectively

const ApplicationDispatch = React.createContext()
const ApplicationContext = React.createContext()

function stateReducer(state, action) {
  if (state.hasOwnProperty(action.type)) {
    return { ...state, [action.type]: state[action.type] = action.newValue };
  }
  throw new Error(`Unhandled action type: ${action.type}`);
}

const initialState = {
  keyCode: '',
  testCode: '',
  testMode: false,
  phoneNumber: '',
  resultCode: null,
  mobileInfo: '',
  configName: '',
  appConfig: {},
};

function DispatchProvider({ children }) {
  const [state, dispatch] = React.useReducer(stateReducer, initialState);
  return (
    <ApplicationDispatch.Provider value={dispatch}>
      <ApplicationContext.Provider value={state}>
        {children}
      </ApplicationContext.Provider>
    </ApplicationDispatch.Provider>
  )
}

function useDispatchable(stateName) {
  const context = React.useContext(ApplicationContext);
  const dispatch = React.useContext(ApplicationDispatch);
  return [context[stateName], newValue => dispatch({ type: stateName, newValue })];
}

function useKeyCode() { return useDispatchable('keyCode'); }
function useTestCode() { return useDispatchable('testCode'); }
function useTestMode() { return useDispatchable('testMode'); }
function usePhoneNumber() { return useDispatchable('phoneNumber'); }
function useResultCode() { return useDispatchable('resultCode'); }
function useMobileInfo() { return useDispatchable('mobileInfo'); }
function useConfigName() { return useDispatchable('configName'); }
function useAppConfig() { return useDispatchable('appConfig'); }

export {
  DispatchProvider,
  useKeyCode,
  useTestCode,
  useTestMode,
  usePhoneNumber,
  useResultCode,
  useMobileInfo,
  useConfigName,
  useAppConfig,
}

dengan penggunaan yang mirip dengan ini:

import { useHistory } from "react-router-dom";

// https://react-bootstrap.github.io/components/alerts
import { Container, Row } from 'react-bootstrap';

import { useAppConfig, useKeyCode, usePhoneNumber } from '../../ApplicationDispatchProvider';

import { ControlSet } from '../../components/control-set';
import { keypadClass } from '../../utils/style-utils';
import { MaskedEntry } from '../../components/masked-entry';
import { Messaging } from '../../components/messaging';
import { SimpleKeypad, HandleKeyPress, ALT_ID } from '../../components/simple-keypad';

export const AltIdPage = () => {
  const history = useHistory();
  const [keyCode, setKeyCode] = useKeyCode();
  const [phoneNumber, setPhoneNumber] = usePhoneNumber();
  const [appConfig, setAppConfig] = useAppConfig();

  const keyPressed = btn => {
    const maxLen = appConfig.phoneNumberEntry.entryLen;
    const newValue = HandleKeyPress(btn, phoneNumber).slice(0, maxLen);
    setPhoneNumber(newValue);
  }

  const doSubmit = () => {
    history.push('s');
  }

  const disableBtns = phoneNumber.length < appConfig.phoneNumberEntry.entryLen;

  return (
    <Container fluid className="text-center">
      <Row>
        <Messaging {...{ msgColors: appConfig.pageColors, msgLines: appConfig.entryMsgs.altIdMsgs }} />
      </Row>
      <Row>
        <MaskedEntry {...{ ...appConfig.phoneNumberEntry, entryColors: appConfig.pageColors, entryLine: phoneNumber }} />
      </Row>
      <Row>
        <SimpleKeypad {...{ keyboardName: ALT_ID, themeName: appConfig.keyTheme, keyPressed, styleClass: keypadClass }} />
      </Row>
      <Row>
        <ControlSet {...{ btnColors: appConfig.buttonColors, disabled: disableBtns, btns: [{ text: 'Submit', click: doSubmit }] }} />
      </Row>
    </Container>
  );
};

AltIdPage.propTypes = {};

Sekarang semuanya tetap lancar di mana-mana di semua halaman saya

Bagus!

Kent terima kasih!

Al Joslin
sumber
-2
// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;

Sekarang Anda harus melihat, bahwa kode Anda benar - benar berfungsi. Yang tidak berfungsi adalah console.log(movies). Ini karena moviesmenunjuk ke keadaan lama . Jika Anda memindahkan bagian console.log(movies)luar useEffect, tepat di atas pengembalian, Anda akan melihat objek film yang diperbarui.

Nikita Malyschkin
sumber