Як використовувати зворотний виклик `setState` на реакційних гачках


134

Введення в React hooks useStateдля встановлення стану компонента. Але як я можу використовувати хуки для заміни зворотного виклику, як показано нижче:

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

Я хочу щось зробити після оновлення штату.

Я знаю, що можу використовувати useEffectзайві речі, але я повинен перевірити стан попереднього значення, яке вимагає бітовий код. Я шукаю просте рішення, яке можна використовувати з useStateгачком.


1
у компоненті класу я використовував async і чекаю досягнення того самого результату, як те, що ви зробили для додавання зворотного виклику в setState. На жаль, це не працює в гачку. Навіть якщо я додав async і await, реакція не буде чекати оновлення стану. Можливо, useEffect - це єдиний спосіб це зробити.
MING WU

2
@Zhao, ти ще не позначив правильну відповідь. Чи можете ви
залишити

Відповіді:


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і вирішити, чи хочете ви щось робити тоді. Чи вважатиме це найкращою практикою?
Дарріл Янг

2
Щоб уникнути запуску useEffectна початковому візуалізації, ви можете створити власний useEffectхук , який не запускається на початковому візуалізації. Для того, щоб створити такий гачок, ви можете перевірити це питання: stackoverflow.com/questions/53253940 / ...

14
А як щодо випадку, коли я хочу викликати різні зворотні виклики в різних викликах setState, які змінять одне і те ж значення стану? Ви відповідаєте неправильно, і в не повинні бути позначені як правильні, щоб не заплутати новачків. Правда полягає в тому, що setState повертає свою найскладнішу проблему під час переходу на хуки з класів, який не має жодного чіткого методу вирішення. Іноді вам дійсно буде достатньо якогось ефекту, що залежить від вартості, а іноді для цього потрібні деякі хакі-методи, наприклад, збереження певних прапорів у посиланнях.
Дмитро Лобов

Очевидно, сьогодні я міг би зателефонувати setCounter (значення, зворотний виклик), щоб якесь завдання було виконане після оновлення стану, уникаючи useEffect. Я не зовсім впевнений, це перевага чи особлива вигода.
Кен Інграм

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().
Грег Садецький

25

Якщо ви хочете оновити попередній стан, ви можете зробити так у хуках:

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


setCount(previousCount => previousCount + 1);

2
Я думаю, setCounterви маєте на увазіsetCount
Мехді Деггані

@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найкраще буде імітувати зворотний дзвінок, а не спрацьовувати для початкового стану. Ви можете витягти з нього спеціальний хук:

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 Я думаю, що в частині Usage, stateпосилання на попередню ще є, коли викликається зворотний виклик. Чи не так?
MJ Studio

2

setState() витримує зміни у стані компонента та повідомляє React, що цей компонент та його дочірні елементи повинні бути повторно відтворені з оновленим станом.

setState метод є асинхронним, і насправді він не повертає обіцянку. Отже, у випадках, коли ми хочемо оновити або викликати функцію, функцію можна викликати зворотним викликом у функції setState як другий аргумент. Наприклад, у вашому випадку вище, ви викликали функцію як зворотний виклик setState.

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

Наведений вище код чудово працює для компонента класу, але у випадку функціонального компонента ми не можемо використовувати метод setState, і це може використовувати хук ефекту використання для досягнення того самого результату.

Очевидний метод, який приходить на думку, полягає в тому, що ypu можна використовувати з useEffect, як показано нижче:

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

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

Але це також спрацьовує на першому візуалізації, тому ми можемо змінити код наступним чином, де ми можемо перевірити першу подію візуалізації та уникнути стану візуалізації. Тому реалізацію можна здійснити наступним чином:

Тут ми можемо використати хук користувача для ідентифікації першого візуалізації.

Хук useRef дозволяє нам створювати змінні змінні у функціональних компонентах. Це корисно для доступу до DOM-вузлів / елементів React та для зберігання змінних змінних, не викликаючи повторного відтворення.

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 при використанні хуків?


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 виконувався за допомогою гачків, використовуйте змінну прапора та надайте IF ELSE або IF блок всередині useEffect, щоб, коли ці умови виконуються, виконувався лише цей блок коду. Ефект, який рази разиться, виконується у міру зміни масиву залежностей, але цей IF-код всередині ефекту буде виконуватися лише за певних умов.


4
Це не спрацює. Ви не знаєте, в якому порядку три оператори всередині updateAge насправді працюватимуть. Усі три є асинхронними. Єдине, що гарантується, це те, що перший рядок працює до 3-го (оскільки вони працюють в одному штаті). Ви нічого не знаєте про 2-й рядок. Цей приклад занадто простий, щоб бачити це.
Мохіт Сінгх,

Мій друг мохіт. Я застосував цю техніку у великому комплексному проекті реагування, коли я переходив від класів реакцій до хуків, і він працює чудово. Просто спробуйте ту саму логіку в будь-якому місці хуків для заміни зворотного виклику setState, і ви дізнаєтесь.
Арджун Сах,

4
"роботи в моєму проекті не є поясненням", прочитайте документи. Вони взагалі не синхронні. Ви не можете точно сказати, що три рядки в updateAge працювали б у такому порядку. Якщо він синхронізувався, тоді що потрібно прапору, безпосередньо викликайте рядок console.log () після setAge.
Мохіт Сінгх,

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 - основне рішення. Але, як згадував Дарріл, використання 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 - це асинхронізація, ви не знаєте, чи буде викликано console.log після setCounter або раніше.
Мохіт Сінгх,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.