วิธีใช้ "setState" callback บน react hooks


134

React hooks แนะนำuseStateสำหรับการตั้งค่าสถานะส่วนประกอบ แต่ฉันจะใช้ hooks เพื่อแทนที่การโทรกลับเช่นรหัสด้านล่างได้อย่างไร:

setState(
  { name: "Michael" },
  () => console.log(this.state)
);

ฉันต้องการทำอะไรบางอย่างหลังจากอัปเดตสถานะแล้ว

ฉันรู้ว่าฉันสามารถใช้useEffectทำสิ่งพิเศษได้ แต่ฉันต้องตรวจสอบสถานะก่อนหน้าซึ่งต้องใช้รหัสบิต ฉันกำลังมองหาวิธีง่ายๆที่สามารถใช้กับuseStateตะขอได้


1
ในองค์ประกอบคลาสฉันใช้ async และรอเพื่อให้ได้ผลลัพธ์เช่นเดียวกับที่คุณทำเพื่อเพิ่มการโทรกลับใน setState น่าเสียดายที่มันไม่ทำงานในตะขอ แม้ว่าฉันจะเพิ่ม async และรอการตอบสนองจะไม่รอให้สถานะอัปเดต บางที useEffect เป็นวิธีเดียวที่จะทำได้
MING WU

2
@ จ้าวคุณยังไม่ได้ทำเครื่องหมายคำตอบที่ถูกต้อง ขอเวลาสักสองสามวินาทีได้ไหม
Zohaib Ijaz

คำตอบ:


151

คุณต้องใช้useEffectเบ็ดเพื่อให้บรรลุสิ่งนี้

const [counter, setCounter] = useState(0);

const doSomething = () => {
  setCounter(123);
}

useEffect(() => {
   console.log('Do something after counter has changed', counter);
}, [counter]);

55
การดำเนินการนี้จะเริ่มการconsole.logแสดงผลครั้งแรกตลอดจนcounterการเปลี่ยนแปลงเวลาใด ๆ จะเกิดอะไรขึ้นหากคุณต้องการทำบางสิ่งหลังจากที่สถานะได้รับการอัปเดตแล้ว แต่ไม่ได้อยู่ในการแสดงผลครั้งแรกเนื่องจากตั้งค่าเริ่มต้นไว้ ฉันเดาว่าคุณสามารถตรวจสอบค่าuseEffectและตัดสินใจได้ว่าคุณต้องการทำอะไร จะถือเป็นแนวทางปฏิบัติที่ดีที่สุดหรือไม่?
Darryl Young

2
เพื่อหลีกเลี่ยงการทำงานuseEffectในการเรนเดอร์เริ่มต้นคุณสามารถสร้างhook ที่กำหนดเองuseEffectซึ่งไม่ทำงานในการแสดงผลครั้งแรก หากต้องการสร้างเบ็ดดังกล่าวคุณสามารถดูคำถามนี้: stackoverflow.com/questions/53253940/…

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

เห็นได้ชัดว่าวันนี้ฉันสามารถเรียกใช้ setCounter (ค่าการโทรกลับ) เพื่อให้งานบางอย่างถูกดำเนินการหลังจากอัปเดตสถานะโดยหลีกเลี่ยงการใช้ useEffect ฉันไม่แน่ใจทั้งหมดว่านี่เป็นความชอบหรือผลประโยชน์เฉพาะ
Ken Ingram

2
@KenIngram ฉันเพิ่งลองและได้รับข้อผิดพลาดที่เฉพาะเจาะจงนี้:Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
Greg Sadetsky

26

หากคุณต้องการอัปเดตสถานะก่อนหน้าคุณสามารถทำได้ใน hooks:

const [count, setCount] = useState(0);


setCount(previousCount => previousCount + 1);

2
ฉันคิดว่าsetCounterคุณหมายถึงsetCount
Mehdi Dehghani

@BimalGrg นี้ได้ผลจริงหรือ? ฉันไม่สามารถจำลองสิ่งนี้ได้ ไม่สามารถรวบรวมข้อผิดพลาดนี้:expected as assignment or function call and instead saw an expression
tonitone120

@ tonitone120 มีฟังก์ชันลูกศรอยู่ใน setCount
Bimal Grg

@BimalGrg คำถามกำลังถามถึงความสามารถในการรันโค้ดทันทีหลังจากอัพเดตสถานะ รหัสนี้ช่วยงานนั้นจริงหรือ?
tonitone120

ใช่มันจะได้ผลเช่นนี้ setState ในส่วนประกอบของคลาส
Aashiq ahmed

19

useEffectซึ่งจะเริ่มทำงานเมื่ออัปเดตสถานะเท่านั้น:

const [state, setState] = useState({ name: "Michael" })
const isFirstRender = useRef(true)

useEffect(() => {
  if (!isFirstRender.current) {
    console.log(state) // do something after state has updated
  }
}, [state])

useEffect(() => { 
  isFirstRender.current = false // toggle flag after first render/mounting
}, [])

ด้านบนจะเลียนแบบการsetStateโทรกลับได้ดีที่สุดและไม่เริ่มทำงานสำหรับสถานะเริ่มต้น คุณสามารถแยก Hook แบบกำหนดเองได้:

function useEffectUpdate(effect, deps) {
  const isFirstRender = useRef(true) 

  useEffect(() => {
    if (!isFirstRender.current) {
      effect()
    }  
  }, deps) // eslint-disable-line react-hooks/exhaustive-deps

  useEffect(() => {
    isFirstRender.current = false;
  }, []);
}

// ... Usage inside component
useEffectUpdate(() => { console.log(state) }, [state])

ทางเลือก: useState สามารถดำเนินการเพื่อรับการติดต่อกลับเหมือนsetStateในชั้นเรียน
ford04

18

ฉันคิดว่าการใช้useEffectไม่ใช่วิธีที่เข้าใจง่าย

ฉันสร้างเสื้อคลุมสำหรับสิ่งนี้ ในเบ็ดที่กำหนดเองนี้คุณสามารถส่งการโทรกลับไปที่setStateพารามิเตอร์แทนuseStateพารามิเตอร์

ฉันเพิ่งสร้างเวอร์ชัน typescript ดังนั้นหากคุณต้องการใช้สิ่งนี้ใน Javascript ให้ลบสัญกรณ์บางประเภทออกจากโค้ด

การใช้งาน

const [state, setState] = useStateCallback(1);
setState(2, (n) => {
  console.log(n) // 2
});

คำประกาศ

import { SetStateAction, useCallback, useEffect, useRef, useState } from 'react';

type Callback<T> = (value?: T) => void;
type DispatchWithCallback<T> = (value: T, callback?: Callback<T>) => void;

function useStateCallback<T>(initialState: T | (() => T)): [T, DispatchWithCallback<SetStateAction<T>>] {
  const [state, _setState] = useState(initialState);

  const callbackRef = useRef<Callback<T>>();
  const isFirstCallbackCall = useRef<boolean>(true);

  const setState = useCallback((setStateAction: SetStateAction<T>, callback?: Callback<T>): void => {
    callbackRef.current = callback;
    _setState(setStateAction);
  }, []);

  useEffect(() => {
    if (isFirstCallbackCall.current) {
      isFirstCallbackCall.current = false;
      return;
    }
    callbackRef.current?.(state);
  }, [state]);

  return [state, setState];
}

export default useStateCallback;

ข้อเสียเปรียบ

หากฟังก์ชันลูกศรที่ส่งผ่านอ้างอิงถึงฟังก์ชันภายนอกของตัวแปรฟังก์ชันนั้นจะจับค่าปัจจุบันไม่ใช่ค่าหลังจากที่มีการอัปเดตสถานะ ในตัวอย่างการใช้งานข้างต้นconsole.log (state)จะพิมพ์ 1 ไม่ใช่ 2


1
ขอบคุณ! แนวทางที่ยอดเยี่ยม สร้างโค้ด
ValYouW

@ValYouW ขอบคุณสำหรับการสร้างตัวอย่าง เรื่องเดียวคือถ้าฟังก์ชันลูกศรที่ส่งผ่านไปอ้างอิงฟังก์ชันภายนอกของตัวแปรฟังก์ชันนั้นจะจับค่าปัจจุบันไม่ใช่ค่าหลังจากอัปเดตสถานะ
MJ Studio

ใช่นั่นเป็นส่วนที่น่าดึงดูดด้วยส่วนประกอบที่ใช้งานได้ ...
ValYouW

จะรับค่าสถานะก่อนหน้าได้อย่างไร?
Ankan-Zerob

@ Ankan-Zerob ฉันคิดว่าในส่วนการใช้งานstateกำลังอ้างถึงก่อนหน้านี้ แต่เมื่อมีการโทรกลับ ไม่ใช่เหรอ?
MJ Studio

2

setState() กำหนดให้เปลี่ยนสถานะคอมโพเนนต์และบอก React ว่าคอมโพเนนต์นี้และลูกของมันจำเป็นต้องแสดงผลอีกครั้งด้วยสถานะที่อัพเดต

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

setState(
  { name: "Michael" },
  () => console.log(this.state)
);

โค้ดด้านบนใช้งานได้ดีสำหรับองค์ประกอบคลาส แต่ในกรณีของส่วนประกอบการทำงานเราไม่สามารถใช้เมธอด setState ได้และเราสามารถใช้ use effect hook เพื่อให้ได้ผลลัพธ์เดียวกัน

วิธีการที่ชัดเจนที่นึกถึงคือ ypu สามารถใช้กับ useEffect ได้ดังนี้:

const [state, setState] = useState({ name: "Michael" })

useEffect(() => {
  console.log(state) // do something after state has updated
}, [state])

แต่สิ่งนี้จะเริ่มขึ้นในการเรนเดอร์ครั้งแรกเช่นกันดังนั้นเราจึงสามารถเปลี่ยนรหัสได้ดังนี้ซึ่งเราสามารถตรวจสอบเหตุการณ์การเรนเดอร์แรกและหลีกเลี่ยงการเรนเดอร์สถานะ ดังนั้นการใช้งานสามารถทำได้ด้วยวิธีต่อไปนี้:

เราสามารถใช้ user hook ที่นี่เพื่อระบุการเรนเดอร์แรก

useRef Hook ช่วยให้เราสร้างตัวแปรที่ไม่แน่นอนในส่วนประกอบที่ใช้งานได้ มีประโยชน์สำหรับการเข้าถึงโหนด DOM / องค์ประกอบการตอบสนองและเพื่อจัดเก็บตัวแปรที่เปลี่ยนแปลงได้โดยไม่ต้องทริกเกอร์การเรนเดอร์ซ้ำ

const [state, setState] = useState({ name: "Michael" });
const firstTimeRender = useRef(true);

useEffect(() => {
 if (!firstTimeRender.current) {
    console.log(state);
  }
}, [state])

useEffect(() => { 
  firstTimeRender.current = false 
}, [])

1

ฉันพบปัญหาเดียวกันโดยใช้ useEffect ในการตั้งค่าของฉันไม่ได้ทำเคล็ดลับ (ฉันกำลังอัปเดตสถานะของผู้ปกครองจากองค์ประกอบย่อยของอาร์เรย์หลายองค์ประกอบและฉันต้องการทราบว่าส่วนประกอบใดอัปเดตข้อมูล)

การห่อ setState ในสัญญาอนุญาตให้เรียกใช้การดำเนินการโดยพลการหลังจากเสร็จสิ้น:

import React, {useState} from 'react'

function App() {
  const [count, setCount] = useState(0)

  function handleClick(){
    Promise.resolve()
      .then(() => { setCount(count => count+1)})
      .then(() => console.log(count))
  }


  return (
    <button onClick= {handleClick}> Increase counter </button>
  )
}

export default App;

คำถามต่อไปนี้ทำให้ฉันไปในทิศทางที่ถูกต้อง: React batch state update functions เมื่อใช้ hooks หรือไม่?


0

คำถามของคุณถูกต้องมากขอให้ฉันบอกคุณว่า useEffect ทำงานหนึ่งครั้งตามค่าเริ่มต้นและทุกครั้งที่อาร์เรย์การอ้างอิงเปลี่ยนไป

ตรวจสอบตัวอย่างด้านล่าง ::

import React,{ useEffect, useState } from "react";

const App = () => {
  const [age, setAge] = useState(0);
  const [ageFlag, setAgeFlag] = useState(false);

  const updateAge = ()=>{
    setAgeFlag(false);
    setAge(age+1);
    setAgeFlag(true);
  };

  useEffect(() => {
    if(!ageFlag){
      console.log('effect called without change - by default');
    }
    else{
      console.log('effect called with change ');
    }
  }, [ageFlag,age]);

  return (
    <form>
      <h2>hooks demo effect.....</h2>
      {age}
      <button onClick={updateAge}>Text</button>
    </form>
  );
}

export default App;

หากคุณต้องการให้การเรียกกลับ setState ดำเนินการด้วย hooks ให้ใช้ตัวแปรแฟล็กและกำหนดบล็อก IF ELSE หรือ IF ภายใน useEffect เพื่อให้เมื่อเงื่อนไขนั้นเป็นที่พอใจแล้วให้ดำเนินการบล็อกโค้ดนั้นเท่านั้น อย่างไรก็ตามเวลาที่เอฟเฟกต์ทำงานเมื่ออาร์เรย์อ้างอิงเปลี่ยนแปลง แต่โค้ด IF ภายในเอฟเฟกต์จะดำเนินการเฉพาะในเงื่อนไขนั้น ๆ เท่านั้น


4
วิธีนี้ใช้ไม่ได้ คุณไม่รู้ว่าคำสั่งทั้งสามใน updateAge จะใช้งานได้จริงในลำดับใด ทั้งสามไม่ตรงกัน สิ่งเดียวที่รับประกันได้คือบรรทัดแรกจะทำงานก่อนที่ 3 (เนื่องจากทำงานในสถานะเดียวกัน) คุณไม่รู้อะไรเกี่ยวกับบรรทัดที่ 2 ตัวอย่างนี้ง่ายเกินไปที่จะเห็นสิ่งนี้
Mohit Singh

เพื่อนของฉัน mohit ฉันได้ใช้เทคนิคนี้ในโปรเจ็กต์ปฏิกิริยาที่ซับซ้อนขนาดใหญ่เมื่อฉันย้ายจากคลาสการตอบสนองไปยังตะขอและมันก็ทำงานได้อย่างสมบูรณ์ เพียงลองใช้ตรรกะเดียวกันที่ใดก็ได้ใน hooks เพื่อแทนที่การเรียกกลับ setState แล้วคุณจะรู้
arjun sah

4
"ทำงานในโครงการของฉันไม่ใช่คำอธิบาย" อ่านเอกสาร พวกเขาไม่ซิงโครนัสเลย คุณไม่สามารถพูดได้อย่างแน่นอนว่าสามบรรทัดใน updateAge จะทำงานตามลำดับนั้น หากซิงค์แล้วสิ่งที่ต้องการแฟล็กให้โทรไปที่บรรทัด console.log () หลัง setAge โดยตรง
Mohit Singh

useRef เป็นโซลูชันที่ดีกว่ามากสำหรับ "ageFlag"
Dr.Flink

0

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

import { useState, useCallback, useRef, useEffect } from 'react';
import _isFunction from 'lodash/isFunction';
import _noop from 'lodash/noop';

export const useStateWithCallback = initialState => {
  const [state, setState] = useState(initialState);
  const callbackRef = useRef(_noop);

  const handleStateChange = useCallback((updatedState, callback) => {
    setState(updatedState);
    if (_isFunction(callback)) callbackRef.current = callback;
  }, []);

  useEffect(() => {
    callbackRef.current();
    callbackRef.current = _noop; // to clear the callback after it is executed
  }, [state]);

  return [state, handleStateChange];
};

-3

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

const [counter, setCounter] = useState(0);

const doSomething = () => {
  const updatedNumber = 123;
  setCounter(updatedNumber);

  // now you can "do something" with updatedNumber and don't have to worry about the async nature of setState!
  console.log(updatedNumber);
}

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