useState set method ไม่สะท้อนการเปลี่ยนแปลงทันที


168

ฉันพยายามเรียนรู้ hooks และuseStateวิธีการทำให้ฉันสับสน ฉันกำลังกำหนดค่าเริ่มต้นให้กับรัฐในรูปแบบของอาร์เรย์ วิธีการในการตั้งค่าuseStateจะไม่ทำงานสำหรับฉันแม้จะมีหรือspread(...) without spread operatorฉันได้ทำ API บนพีซีเครื่องอื่นที่ฉันกำลังเรียกและดึงข้อมูลที่ฉันต้องการตั้งค่าเป็นสถานะ

นี่คือรหัสของฉัน:

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

setMovies(result)เช่นเดียวกับsetMovies(...result)ที่ไม่ได้ทำงาน สามารถใช้ความช่วยเหลือที่นี่

ฉันคาดว่าตัวแปรผลลัพธ์จะถูกผลักเข้าไปในอาร์เรย์ภาพยนตร์

คำตอบ:


221

เหมือน setState ในส่วนประกอบของคลาสที่สร้างขึ้นโดยการขยายReact.ComponentหรือReact.PureComponentการอัพเดตสถานะโดยใช้ updater ที่มีให้โดยuseStatehook ก็เป็นแบบอะซิงโครนัสและจะไม่สะท้อนทันที

นอกจากนี้ยังมีปัญหาหลักที่นี่ไม่ได้เป็นเพียงธรรมชาติที่ไม่ตรงกัน แต่ความจริงที่ว่าค่าของรัฐจะถูกใช้โดยฟังก์ชั่นพื้นฐานเกี่ยวกับการปิดปัจจุบันของพวกเขาและการปรับปรุงรัฐจะสะท้อนให้เห็นในครั้งต่อไปอีกครั้งทำให้โดยที่ฝาปิดที่มีอยู่จะไม่ได้รับผลกระทบ แต่คนใหม่จะถูกสร้างขึ้น ขณะนี้อยู่ในสถานะปัจจุบันค่าใน hooks จะได้รับจากการปิดที่มีอยู่และเมื่อเกิดการแสดงผลซ้ำการปิดจะได้รับการอัปเดตตามฟังก์ชั่นที่ถูกสร้างขึ้นใหม่อีกครั้งหรือไม่

แม้ว่าคุณจะเพิ่มsetTimeoutฟังก์ชั่นแม้ว่าการหมดเวลาจะทำงานหลังจากผ่านช่วงเวลาหนึ่งที่การเรนเดอร์ใหม่จะเกิดขึ้น แต่ setTimout จะยังคงใช้ค่าจากการปิดครั้งก่อน แต่ไม่ใช่การอัปเดต

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

หากคุณต้องการดำเนินการกับการอัพเดตสถานะคุณต้องใช้ useEffect hook เหมือนกับการใช้componentDidUpdateในส่วนประกอบของคลาสเนื่องจาก setter ที่ส่งคืนโดย useState ไม่มีรูปแบบการโทรกลับ

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

ตราบใดที่ยังคำนึงถึงไวยากรณ์ในการอัปเดตสถานะsetMovies(result)จะแทนที่moviesค่าก่อนหน้านี้ในสถานะด้วยสถานะที่มีอยู่จากคำขอ async

อย่างไรก็ตามหากคุณต้องการรวมการตอบกลับด้วยค่าที่มีอยู่ก่อนหน้านี้คุณต้องใช้ไวยากรณ์การเรียกกลับของการอัพเดทสถานะพร้อมกับการใช้งานที่ถูกต้องของไวยากรณ์การแพร่กระจายเช่น

setMovies(prevMovies => ([...prevMovies, ...result]));

17
สวัสดีสิ่งที่เกี่ยวกับการโทรการใช้งานรัฐอยู่ในรูปแบบการส่งตัวจัดการ? ฉันทำงานในการตรวจสอบรูปแบบที่ซับซ้อนและฉันโทรภายใน submitHandler ใช้ hooks รัฐและการเปลี่ยนแปลงโชคไม่ได้ทันที!
da45

2
useEffectอาจไม่ใช่โซลูชันที่ดีที่สุดเนื่องจากไม่รองรับการโทรแบบอะซิงโครนัส ดังนั้นหากเราต้องการตรวจสอบความถูกต้องแบบอะซิงโครนัสเกี่ยวกับmoviesการเปลี่ยนสถานะเราไม่สามารถควบคุมมันได้
RA

2
โปรดทราบว่าในขณะที่คำแนะนำนั้นดีมากคำอธิบายของสาเหตุสามารถปรับปรุงได้ - ไม่เกี่ยวข้องกับข้อเท็จจริงที่ว่าthe updater provided by useState hook อะซิงโครนัสหรือไม่นั้นต่างจากthis.stateการกลายพันธุ์หากthis.setStateซิงโครนัสการปิดรอบconst moviesจะยังคงเหมือนเดิมuseStateให้ฟังก์ชั่นแบบซิงโครนัส - ดูตัวอย่างในคำตอบของฉัน
Aprillion

setMovies(prevMovies => ([...prevMovies, ...result]));ทำงานให้ฉัน
Mihir

มันกำลังบันทึกผลลัพธ์ที่ผิดเพราะคุณกำลังบันทึกการปิดค้างไม่ใช่เพราะ setter นั้นไม่ตรงกัน หาก async เป็นปัญหาคุณสามารถเข้าสู่ระบบหลังจากหมดเวลา แต่คุณสามารถตั้งค่าการหมดเวลาเป็นเวลาหนึ่งชั่วโมงและยังคงบันทึกผลลัพธ์ที่ไม่ถูกต้องเพราะ async ไม่ใช่สิ่งที่ทำให้เกิดปัญหา
HMR

127

รายละเอียดเพิ่มเติมของคำตอบก่อนหน้านี้ :

ในขณะที่ React's setState นั้นเป็นแบบอะซิงโครนัส (ทั้งคลาสและ hooks) และมันเป็นการล่อลวงให้ใช้ความจริงนั้นเพื่ออธิบายพฤติกรรมที่สังเกตได้มันไม่ใช่เหตุผลว่าทำไมมันจึงเกิดขึ้น

TLDR: เหตุผลคือขอบเขตการปิดรอบconstค่าที่ไม่เปลี่ยนรูป


Solutions:

  • อ่านค่าในฟังก์ชั่นการแสดงผล (ไม่อยู่ในฟังก์ชั่นที่ซ้อนกัน):

    useEffect(() => { setMovies(result) }, [])
    console.log(movies)
  • เพิ่มตัวแปรเข้าสู่การพึ่งพา (และใช้กฎreact-hooks / exhaustive-deps eslint):

    useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
  • ใช้การอ้างอิงที่ไม่แน่นอน (เมื่อเป็นไปไม่ได้ข้างต้น):

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

คำอธิบายว่าทำไมมันเกิดขึ้น:

หาก async await setState()เป็นเหตุผลเดียวที่มันจะเป็นไปได้

howerver ทั้งสองpropsและstateจะถือว่าไม่มีการเปลี่ยนแปลงระหว่างวันที่ 1 Render

ปฏิบัติthis.stateราวกับว่ามันไม่เปลี่ยนรูป

ด้วย hooks สมมติฐานนี้ได้รับการปรับปรุงโดยใช้ค่าคงที่กับconstคำหลัก:

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

ค่าอาจแตกต่างกันระหว่าง 2 เรนเดอร์ แต่ยังคงเป็นค่าคงที่ภายในตัวเรนเดอร์และภายในการปิดใด ๆ(ฟังก์ชั่นที่มีอายุการใช้งานยาวนานกว่าหลังจากเรนเดอร์เสร็จสิ้นเช่นuseEffectตัวจัดการเหตุการณ์

พิจารณาการใช้งานปลอม แต่ซิงโครนัสการโต้ตอบแบบตอบสนอง

// 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>


12
คำตอบที่ยอดเยี่ยม IMO คำตอบนี้จะช่วยให้คุณเสียใจมากที่สุดในอนาคตหากคุณลงทุนเวลามากพอที่จะอ่านและดูดซับ
Scott Mallon

มันทำให้ยากมากที่จะใช้บริบทร่วมกับเราเตอร์ใช่ไหม ในขณะที่ฉันไปที่หน้าถัดไปรัฐจะหายไป .. และฉันไม่สามารถตั้งค่าสถานะภายในการใช้งานได้ผลตะขอเนื่องจากพวกเขาไม่สามารถแสดง ... ฉันขอขอบคุณความสมบูรณ์ของคำตอบและมันอธิบายสิ่งที่ฉันเห็น แต่ฉัน ไม่สามารถหาวิธีเอาชนะมันได้ ฉันกำลังส่งฟังก์ชั่นในบริบทเพื่ออัปเดตค่าที่ไม่คงอยู่ได้อย่างมีประสิทธิภาพ ... ดังนั้นฉันต้องทำให้พวกเขาหลุดพ้นจากการปิดกั้นในบริบทใช่หรือไม่
อัล Joslin

@AlJoslin ได้อย่างรวดเร็วก่อนดูเหมือนว่าจะมีปัญหาแยกต่างหากแม้ว่ามันอาจจะเกิดจากขอบเขตการปิด หากคุณมีคำถามที่เป็นรูปธรรมโปรดสร้างคำถามส
แต็คโอเวอร์โฟลว์

1
อันที่จริงฉันเพิ่งเขียนใหม่เสร็จสิ้นด้วย useReducer ตามบทความ @kentcdobs (อ้างอิงด้านล่าง) ซึ่งให้ผลลัพธ์ที่มั่นคงแก่ฉันจริง ๆ ซึ่งไม่ได้รับผลกระทบเพียงเล็กน้อยจากปัญหาการปิดเหล่านี้ (อ้างอิง: kentcdodds.com/blog/how-to-use-react-context-effectively )
Al Joslin

1

ฉันเพิ่งเขียนซ้ำด้วย useReducer ตามบทความ @kentcdobs (อ้างอิงด้านล่าง) ซึ่งให้ผลลัพธ์ที่มั่นคงแก่ฉันจริง ๆ ซึ่งไม่ได้รับผลกระทบเพียงเล็กน้อยจากปัญหาการปิดเหล่านี้

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

ฉันย่อแผ่นสำเร็จรูปที่อ่านได้ของเขาให้อยู่ในระดับที่ต้องการ DRYness การอ่านการติดตั้ง Sandbox ของเขาจะแสดงให้คุณเห็นว่ามันใช้งานได้จริงอย่างไร

สนุกฉันรู้ว่าฉัน !!

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,
}

ด้วยการใช้งานที่คล้ายกับสิ่งนี้:

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 = {};

ตอนนี้ทุกอย่างยังคงราบรื่นทุกที่ในทุกหน้าของฉัน

ดี!

ขอบคุณเคนต์!


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

ตอนนี้คุณควรจะเห็นว่ารหัสของคุณจริงไม่ทำงาน console.log(movies)สิ่งที่ไม่ทำงานเป็น เพราะนี่คือmoviesจุดไปยังรัฐเก่า หากคุณย้ายconsole.log(movies)ออกuseEffectไปข้างนอกเหนือการส่งคืนคุณจะเห็นวัตถุภาพยนตร์ที่อัปเดต

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.