Jak używać wywołania zwrotnego `setState` na zaczepach reagowania

134

React hooks wprowadza useStatedo ustawiania stanu komponentu. Ale jak mogę użyć hooków, aby zamienić wywołanie zwrotne, jak poniżej:

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

Chcę coś zrobić po zaktualizowaniu stanu.

Wiem, że mogę useEffectzrobić dodatkowe rzeczy, ale muszę sprawdzić poprzednią wartość stanu, która wymaga kodu bitowego. Szukam prostego rozwiązania, które można zastosować z useStatehakiem.

Joey Yi Zhao
źródło
1
w komponencie klasy użyłem async i czekam na osiągnięcie tego samego wyniku, co w przypadku dodania wywołania zwrotnego w setState. Niestety nie działa w haku. Nawet jeśli dodałem async i czekam, reaguj nie będzie czekać na aktualizację stanu. Może useEffect to jedyny sposób, aby to zrobić.
MING WU
2
@Zhao, jeszcze nie zaznaczyłeś poprawnej odpowiedzi. Czy możesz uprzejmie poświęcić kilka sekund
Zohaib Ijaz

Odpowiedzi:

151

Aby useEffectto osiągnąć, musisz użyć haka.

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

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

useEffect(() => {
   console.log('Do something after counter has changed', counter);
}, [counter]);
Zohaib Ijaz
źródło
55
Spowoduje to uruchomienie console.logprzy pierwszym renderowaniu, a także przy każdej counterzmianie czasu . A co, jeśli chcesz coś zrobić dopiero po zaktualizowaniu stanu, ale nie przy pierwszym renderowaniu, gdy ustawiona jest wartość początkowa? Myślę, że możesz sprawdzić wartość useEffecti zdecydować, czy chcesz wtedy coś zrobić. Czy byłoby to uznane za najlepszą praktykę?
Darryl Young
2
Aby uniknąć uruchamiania useEffectprzy początkowym renderowaniu, możesz utworzyć niestandardowyuseEffect punkt zaczepienia , który nie jest uruchamiany podczas początkowego renderowania. Aby stworzyć taki hook, możesz sprawdzić to pytanie: stackoverflow.com/questions/53253940/…
14
A co z przypadkiem, gdy chcę wywołać różne wywołania zwrotne w różnych wywołaniach setState, które zmienią tę samą wartość stanu? Twoja odpowiedź jest nieprawidłowa i nie powinna być oznaczona jako poprawna, aby nie wprowadzać w błąd początkujących. Prawda jest taka, że ​​wywołania zwrotne setState są jednym z najtrudniejszych problemów podczas migracji po hookach z klas, które nie mają jednej jasnej metody rozwiązywania. Czasami naprawdę wystarczy trochę efektów zależnych od wartości, a czasami będzie to wymagało pewnych hackerskich metod, takich jak zapisywanie niektórych flag w Refs.
Dmitry Lobov
Najwyraźniej dzisiaj mógłbym wywołać setCounter (value, callback), aby jakieś zadanie było wykonywane po aktualizacji stanu, unikając useEffect. Nie jestem do końca pewien, czy jest to preferencja, czy jakaś szczególna korzyść.
Ken Ingram
2
@KenIngram Właśnie to wypróbowałem i otrzymałem ten bardzo konkretny błąd: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
25

Jeśli chcesz zaktualizować poprzedni stan, możesz to zrobić w hookach:

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


setCount(previousCount => previousCount + 1);
Bimal Grg
źródło
2
Myślę, że setCountermasz na myślisetCount
Mehdi Dehghani
@BimalGrg Czy to naprawdę działa? Nie mogę tego powtórzyć. Nie można skompilować się z tym błędem:expected as assignment or function call and instead saw an expression
tonitone120
@ tonitone120 jest funkcja strzałki wewnątrz setCount.
Bimal Grg
@BimalGrg Pytanie dotyczy możliwości wykonania kodu natychmiast po zaktualizowaniu stanu. Czy ten kod naprawdę pomaga w tym zadaniu?
tonitone120
Tak, to będzie działać, jak w tym. SetState in class components,
Aashiq ahmed
19

useEffect, który uruchamia się tylko przy aktualizacjach stanu :

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

Powyższe będzie setStatenajlepiej naśladować wywołanie zwrotne i nie będzie wywoływać stanu początkowego. Możesz wyodrębnić z niego niestandardowy 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])
ford04
źródło
Alternatywa: useState może być zaimplementowana w celu otrzymania wywołania zwrotnego jak setStatew klasach.
ford04
18

Myślę, że używanie useEffectnie jest intuicyjnym sposobem.

Stworzyłem do tego opakowanie. W tym niestandardowym podpięciu możesz przesłać wywołanie zwrotne do setStateparametru zamiast useStateparametru.

Właśnie stworzyłem wersję Typescript. Więc jeśli chcesz użyć tego w JavaScript, po prostu usuń jakąś notację typu z kodu.

Stosowanie

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

Deklaracja

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;

Wada

Jeśli przekazana funkcja strzałki odwołuje się do zmiennej zewnętrznej funkcji, to po zaktualizowaniu stanu przechwyci bieżącą wartość, a nie wartość. W powyższym przykładzie użycia console.log (stan) wyświetli 1, a nie 2.

MJ Studio
źródło
1
Dzięki! fajne podejście. Utworzono przykładowe kodyandbox
ValYouW
@ValYouW Dziękujemy za stworzenie próbki. Jedyną kwestią jest to, że jeśli przekazana funkcja strzałkowa odwołuje się do zmiennej zewnętrznej funkcji, to po zaktualizowaniu stanu będzie przechwytywać bieżące wartości, a nie wartości.
MJ Studio
Tak, to chwytliwa część z funkcjonalnymi komponentami ...
ValYouW
Jak uzyskać tutaj poprzednią wartość stanu?
Ankan-Zerob
@ Ankan-Zerob Myślę, że w części Użycie stateodnosi się do poprzedniego, gdy wywoływany jest callback. Prawda?
MJ Studio
2

setState() umieszcza w kolejce zmiany stanu składnika i informuje Reacta, że ​​ten składnik i jego elementy potomne muszą zostać ponownie wyrenderowane ze zaktualizowanym stanem.

setState jest asynchroniczna i właściwie nie zwraca obietnicy. Więc w przypadkach, gdy chcemy zaktualizować lub wywołać funkcję, funkcję tę można nazwać callback w funkcji setState jako drugi argument. Na przykład w powyższym przypadku wywołałeś funkcję jako wywołanie zwrotne setState.

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

Powyższy kod działa dobrze dla komponentu klasy, ale w przypadku komponentu funkcjonalnego nie możemy użyć metody setState, a to możemy wykorzystać hook efektu użycia, aby osiągnąć ten sam wynik.

Oczywistą metodą, która przychodzi na myśl, jest to, że ypu może używać z useEffect, jak poniżej:

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

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

Ale uruchomiłoby się to również przy pierwszym renderowaniu, więc możemy zmienić kod w następujący sposób, w którym możemy sprawdzić pierwsze zdarzenie renderowania i uniknąć renderowania stanu. Dlatego wdrożenie można przeprowadzić w następujący sposób:

Możemy tutaj użyć haka użytkownika, aby zidentyfikować pierwszy render.

Hook useRef pozwala nam tworzyć zmienne zmienne w komponentach funkcjonalnych. Przydaje się do uzyskiwania dostępu do węzłów DOM / elementów React i do przechowywania zmiennych zmiennych bez wyzwalania ponownego renderowania.

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

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

useEffect(() => { 
  firstTimeRender.current = false 
}, [])
Laura Nutt
źródło
1

Miałem ten sam problem, użycie useEffect w mojej konfiguracji nie załatwiło sprawy (aktualizuję stan rodzica z tablicy wielu komponentów podrzędnych i muszę wiedzieć, który składnik zaktualizował dane).

Zawijanie setState w obietnicy umożliwia wywołanie dowolnej akcji po zakończeniu:

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;

Następujące pytanie skierowało mnie we właściwym kierunku: Czy aktualizacja stanu wsadowego React działa podczas korzystania z zaczepów?

Hugo Merzisen
źródło
0

Twoje pytanie jest bardzo trafne, powiem ci, że useEffect jest uruchamiany domyślnie raz i po każdej zmianie tablicy zależności.

sprawdź poniższy przykład:

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;

Jeśli chcesz, aby wywołanie zwrotne setState było wykonywane z zaczepami, użyj zmiennej flag i podaj blok IF ELSE LUB IF wewnątrz useEffect, tak aby po spełnieniu tych warunków wykonywany był tylko ten blok kodu. Jakkolwiek czas działa, gdy zmienia się tablica zależności, ale ten kod IF wewnątrz efektu będzie wykonywany tylko w tych określonych warunkach.

arjun sah
źródło
4
To nie zadziała. Nie wiesz, w jakiej kolejności te trzy instrukcje wewnątrz updateAge będą faktycznie działać. Wszystkie trzy są asynchroniczne. Jedyną gwarantowaną rzeczą jest to, że pierwsza linia działa przed trzecią (ponieważ działają w tym samym stanie). Nie wiesz nic o drugiej linii. Ten przykład jest zbyt prosty, aby to zobaczyć.
Mohit Singh
Mój przyjaciel mohit. Zaimplementowałem tę technikę w dużym złożonym projekcie reagowania, kiedy przechodziłem od klas reagowania do hooków i działa doskonale. Po prostu wypróbuj tę samą logikę w dowolnym miejscu w hookach, aby zastąpić wywołanie zwrotne setState i będziesz wiedział.
arjun sah
4
„prace w moim projekcie nie są wyjaśnieniem”, przeczytaj dokumentację. W ogóle nie są synchroniczne. Nie można powiedzieć na pewno, że trzy wiersze w updateAge będą działać w tej kolejności. Jeśli był zsynchronizowany, to czego potrzeba flagi, bezpośrednio wywołaj linię console.log () po setAge.
Mohit Singh
useRef jest znacznie lepszym rozwiązaniem dla „ageFlag”.
Dr Flink
0

Miałem przypadek użycia, w którym chciałem wykonać wywołanie interfejsu API z kilkoma parametrami po ustawieniu stanu. Nie chciałem ustawiać tych parametrów jako swojego stanu, więc zrobiłem niestandardowy hak i oto moje rozwiązanie

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];
};
Akash Singh
źródło
-3

UseEffect jest podstawowym rozwiązaniem. Ale jak wspomniał Darryl, używając useEffect i przekazując stan, ponieważ drugi parametr ma jedną wadę, komponent będzie działał w procesie inicjalizacji. Jeśli chcesz, aby funkcja zwrotna działała z wartością zaktualizowanego stanu, możesz ustawić stałą lokalną i użyć jej zarówno w funkcji setState, jak i 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);
}
Kyle Laster
źródło
5
setCounter jest async, nie wiesz, czy console.log zostanie wywołana po ustawieniu setCounter czy wcześniej.
Mohit Singh