Cách sử dụng lệnh gọi lại `setState` trên các móc phản ứng


119

React hooks giới thiệu useStateđể thiết lập trạng thái thành phần. Nhưng làm cách nào tôi có thể sử dụng hook để thay thế lệnh gọi lại như mã dưới đây:

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

Tôi muốn làm điều gì đó sau khi trạng thái được cập nhật.

Tôi biết tôi có thể sử dụng useEffectđể làm những việc bổ sung nhưng tôi phải kiểm tra giá trị trước đó của trạng thái yêu cầu mã bit. Tôi đang tìm kiếm một giải pháp đơn giản có thể được sử dụng với useStatehook.


1
trong thành phần lớp, tôi đã sử dụng async và chờ đợi để đạt được kết quả tương tự như những gì bạn đã làm để thêm lệnh gọi lại trong setState. Thật không may, nó không hoạt động trong hook. Ngay cả khi tôi đã thêm không đồng bộ và chờ đợi, phản ứng sẽ không đợi trạng thái cập nhật. Có thể sử dụngEffect là cách duy nhất để làm điều đó.
MING WU

2
@Zhao, bạn chưa đánh dấu câu trả lời đúng. Bạn có thể vui lòng dành vài giây
Zohaib Ijaz

Câu trả lời:


128

Bạn cần phải sử dụng useEffecthook để đạt được điều này.

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

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

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

48
Thao tác này sẽ kích hoạt console.logkết xuất đầu tiên cũng như bất kỳ counterthay đổi nào về thời gian . Điều gì sẽ xảy ra nếu bạn chỉ muốn làm điều gì đó sau khi trạng thái đã được cập nhật nhưng không phải trên kết xuất ban đầu khi giá trị ban đầu được đặt? Tôi đoán bạn có thể kiểm tra giá trị useEffectvà quyết định xem bạn có muốn làm điều gì đó sau đó không. Đó có được coi là phương pháp hay nhất không?
Darryl Young

2
Để tránh chạy useEffectkhi kết xuất ban đầu, bạn có thể tạo một móc tùy chỉnhuseEffect , không chạy khi kết xuất ban đầu. Để tạo một hook như vậy, bạn có thể xem câu hỏi này: stackoverflow.com/questions/53253940/…

11
Và trong trường hợp, khi tôi muốn gọi các lệnh gọi lại khác nhau trong các lệnh gọi setState khác nhau, sẽ thay đổi cùng một giá trị trạng thái? Câu trả lời của bạn là sai và không nên đánh dấu câu trả lời là đúng để không gây nhầm lẫn cho người mới. Sự thật là setState gọi lại một trong những vấn đề khó nhất khi di chuyển trên hook từ các lớp không có một phương pháp giải quyết rõ ràng. Đôi khi bạn thực sự cảm thấy khó chịu với một số hiệu ứng tùy thuộc vào giá trị, và đôi khi nó sẽ yêu cầu một số phương pháp hack, như lưu một số loại cờ trong Refs.
Dmitry Lobov

Rõ ràng, hôm nay, tôi có thể gọi setCounter (giá trị, gọi lại) để một số tác vụ được thực thi sau khi trạng thái được cập nhật, tránh bị useEffect. Tôi không hoàn toàn chắc chắn đây là một sở thích hay một lợi ích cụ thể.
Ken Ingram

1
@KenIngram Tôi vừa thử điều đó và gặp lỗi rất cụ thể này: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

22

Nếu bạn muốn cập nhật trạng thái trước đó thì bạn có thể làm như sau trong hooks:

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


setCount(previousCount => previousCount + 1);

2
Ý tôi là của setCounterbạnsetCount
Mehdi Dehghani

15

useEffect, chỉ kích hoạt khi cập nhật trạng thái :

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
}, [])

Ở trên sẽ bắt chước lệnh setStategọi lại tốt nhất và không kích hoạt ở trạng thái ban đầu. Bạn có thể trích xuất một Hook tùy chỉnh từ nó:

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

Thay thế: useState có thể được thực hiện để nhận một cuộc gọi lại như setStatetrong các lớp.
ford04

15

Tôi nghĩ, sử dụng useEffectkhông phải là một cách trực quan.

Tôi đã tạo một trình bao bọc cho việc này. Trong móc tùy chỉnh này, bạn có thể truyền lệnh gọi lại của mình tới setStatetham số thay vì useStatetham số.

Tôi vừa tạo phiên bản Typecript. Vì vậy, nếu bạn cần sử dụng điều này trong Javascript, chỉ cần xóa một số ký hiệu kiểu khỏi mã.

Sử dụng

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

Tờ khai

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;

Hạn chế

Nếu hàm mũi tên được truyền tham chiếu đến một hàm bên ngoài có thể thay đổi, thì nó sẽ nắm bắt giá trị hiện tại không phải là giá trị sau khi trạng thái được cập nhật. Trong ví dụ sử dụng ở trên, console.log (state) sẽ in 1 không phải 2.


1
Cảm ơn! cách tiếp cận mát mẻ. Tạo ra một mẫu codesandbox
ValYouW

@ValYouW Cảm ơn bạn đã tạo mẫu. Vấn đề duy nhất là nếu hàm arrow được truyền tham chiếu đến hàm bên ngoài của biến, thì nó sẽ nắm bắt các giá trị hiện tại chứ không phải các giá trị sau khi trạng thái được cập nhật.
MJ Studio

Vâng, đó là phần hấp dẫn với các thành phần chức năng ...
ValYouW

Làm thế nào để lấy giá trị trạng thái trước đây?
Ankan-Zerob

@ Ankan-Zerob Tôi nghĩ, trong phần Sử dụng, statenó đang đề cập đến cái trước đó khi gọi lại được gọi. Phải không?
MJ Studio

0

Câu hỏi của bạn rất hợp lệ, hãy để tôi nói với bạn rằng useEffect chạy một lần theo mặc định và sau mỗi lần mảng phụ thuộc thay đổi.

kiểm tra ví dụ bên dưới ::

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;

Nếu bạn muốn lệnh gọi lại setState được thực thi với các hook thì hãy sử dụng biến cờ và cung cấp khối IF ELSE OR IF bên trong useEffect để khi các điều kiện đó được thỏa mãn thì chỉ khối mã đó thực thi. Dù thời gian hiệu ứng chạy như thế nào khi mảng phụ thuộc thay đổi nhưng mã IF bên trong có hiệu lực sẽ chỉ thực thi trên các điều kiện cụ thể đó.


3
Điều này sẽ không hoạt động. Bạn không biết ba câu lệnh bên trong updateAge sẽ thực sự hoạt động theo thứ tự nào. Cả ba đều không đồng bộ. Điều duy nhất được đảm bảo là dòng đầu tiên chạy trước dòng thứ 3 (vì chúng hoạt động trên cùng một trạng thái). Bạn không biết gì về dòng thứ 2. Ví dụ này là quá nhỏ để xem điều này.
Mohit Singh

Bạn tôi mohit. Tôi đã thực hiện kỹ thuật này trong một dự án phản ứng phức tạp lớn khi tôi đang chuyển từ các lớp phản ứng sang các móc và nó hoạt động hoàn hảo. Đơn giản chỉ cần thử logic tương tự ở bất kỳ đâu trong hooks để thay thế callback setState và bạn sẽ biết.
arjun sah

3
"hoạt động trong dự án của tôi không phải là một lời giải thích", hãy đọc tài liệu. Chúng không đồng bộ chút nào. Bạn không thể chắc chắn rằng ba dòng trong updateAge sẽ hoạt động theo thứ tự đó. Nếu nó đã được đồng bộ thì có cần gắn cờ gì không, hãy gọi trực tiếp dòng console.log () sau setAge.
Mohit Singh

useRef là một giải pháp tốt hơn nhiều cho "ageFlag".
Dr.Flink

0

setState() Xếp hàng các thay đổi đối với trạng thái thành phần và nói với React rằng thành phần này và các thành phần con của nó cần được hiển thị lại với trạng thái đã cập nhật.

Phương thức setState là không đồng bộ và trên thực tế, nó không trả về một lời hứa. Vì vậy, trong trường hợp chúng ta muốn cập nhật hoặc gọi một hàm, hàm có thể được gọi là hàm callback trong hàm setState làm đối số thứ hai. Ví dụ, trong trường hợp của bạn ở trên, bạn đã gọi một hàm dưới dạng gọi lại setState.

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

Đoạn mã trên hoạt động tốt cho thành phần lớp, nhưng trong trường hợp thành phần chức năng, chúng ta không thể sử dụng phương thức setState và điều này chúng ta có thể sử dụng use effect hook để đạt được kết quả tương tự.

Phương pháp hiển nhiên mà bạn cần lưu ý là ypu có thể sử dụng với useEffect như sau:

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

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

Nhưng điều này cũng sẽ kích hoạt trong lần hiển thị đầu tiên, vì vậy chúng ta có thể thay đổi mã như sau, nơi chúng ta có thể kiểm tra sự kiện kết xuất đầu tiên và tránh trạng thái kết xuất. Do đó, việc triển khai có thể được thực hiện theo cách sau:

Chúng ta có thể sử dụng user hook tại đây để xác định kết xuất đầu tiên.

UseRef Hook cho phép chúng ta tạo các biến có thể thay đổi trong các thành phần chức năng. Nó hữu ích để truy cập các nút DOM / phần tử React và lưu trữ các biến có thể thay đổi mà không cần kích hoạt kết xuất lại.

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

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

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

0

Tôi có một trường hợp sử dụng mà tôi muốn thực hiện một cuộc gọi api với một số tham số sau khi trạng thái được thiết lập. Tôi không muốn đặt các thông số đó làm trạng thái của mình nên tôi đã tạo một móc tùy chỉnh và đây là giải pháp của tôi

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 là giải pháp chính. Nhưng như Darryl đã đề cập, việc sử dụng useEffect và chuyển vào trạng thái như tham số thứ hai có một lỗ hổng, thành phần sẽ chạy trong quá trình khởi tạo. Nếu bạn chỉ muốn hàm gọi lại chạy bằng giá trị của trạng thái đã cập nhật, bạn có thể đặt một hằng số cục bộ và sử dụng hằng số đó trong cả setState và callback.

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

4
setCounter không đồng bộ, bạn không biết console.log sẽ được gọi sau setCounter hay trước đó.
Mohit Singh
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.