metoda set useState nie odzwierciedla natychmiastowej zmiany

168

Próbuję się nauczyć haczyków, a useStatemetoda wprawiła mnie w zakłopotanie. Przypisuję wartość początkową do stanu w postaci tablicy. Metoda set w useStatenie działa nawet z spread(...)lub without spread operator. Zrobiłem API na innym komputerze, który wywołuję i pobieram dane, które chcę ustawić w stanie.

Oto mój kod:

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

Tak setMovies(result)dobrze, jak setMovies(...result)nie działa. Przydałaby się tutaj pomoc.

Oczekuję, że zmienna wynikowa zostanie umieszczona w tablicy filmów.

Pranjal
źródło

Odpowiedzi:

220

Podobnie jak komponenty setState in Class utworzone przez rozszerzenie React.Componentlub React.PureComponent, aktualizacja stanu za pomocą modułu aktualizującego dostarczonego przez useStatehook jest również asynchroniczna i nie będzie natychmiast odzwierciedlać

Również główny problem tutaj nie tylko asynchroniczny charakter, ale fakt, że wartości stanu są używane przez funkcje w oparciu o ich bieżące zamknięcia i aktualizacje stanu, zostaną odzwierciedlone w następnym ponownym renderowaniu, przez które nie ma to wpływu na istniejące zamknięcia, ale zostaną utworzone nowe . Teraz w obecnym stanie wartości wewnątrz punktów zaczepienia są uzyskiwane przez istniejące domknięcia, a po ponownym renderowaniu domknięcia są aktualizowane w oparciu o to, czy funkcja jest ponownie tworzona, czy nie

Nawet jeśli dodasz setTimeoutfunkcję, chociaż limit czasu upłynie po pewnym czasie, po którym nastąpiłoby ponowne renderowanie, ale setTimout nadal będzie używać wartości z poprzedniego zamknięcia, a nie zaktualizowanego.

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

Jeśli chcesz wykonać akcję na aktualizacji stanu, musisz użyć hooka useEffect, podobnie jak componentDidUpdatew przypadku komponentów klasy, ponieważ setter zwracany przez useState nie ma wzorca wywołania zwrotnego

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

Jeśli chodzi o składnię aktualizacji stanu, setMovies(result)zamieni poprzednią movieswartość w stanie na te dostępne z żądania asynchronicznego

Jeśli jednak chcesz scalić odpowiedź z poprzednio istniejącymi wartościami, musisz użyć składni wywołania zwrotnego aktualizacji stanu wraz z poprawnym użyciem składni spreadu, takiej jak

setMovies(prevMovies => ([...prevMovies, ...result]));
Shubham Khatri
źródło
17
Cześć, a co z wywołaniem useState w module obsługi przesyłania formularza? Pracuję nad walidacją złożonego formularza i wywołuję wewnętrzne haki useState w submitHandler i niestety zmiany nie są natychmiastowe!
da45
2
useEffectmoże jednak nie być najlepszym rozwiązaniem, ponieważ nie obsługuje wywołań asynchronicznych. Tak więc, jeśli chcielibyśmy dokonać asynchronicznej weryfikacji movieszmiany stanu, nie mamy nad tym żadnej kontroli.
RA.
2
należy pamiętać, że chociaż rada jest bardzo dobra, wyjaśnienie przyczyny można poprawić - nie ma to nic wspólnego z faktem, czy the updater provided by useState hook jest asynchroniczne, w przeciwieństwie do this.statetego , które mogłoby zostać zmutowane, gdyby this.setStatebyło synchroniczne, Zamknięcie wokół const moviespozostanie takie samo, nawet jeśli useState
podałem
setMovies(prevMovies => ([...prevMovies, ...result]));pracował dla mnie
Mihir
Rejestruje nieprawidłowy wynik, ponieważ rejestrujesz nieaktualne zamknięcie, a nie dlatego, że ustawiający jest asynchroniczny. Jeśli problem był asynchroniczny, możesz zalogować się po przekroczeniu limitu czasu, ale możesz ustawić limit czasu na godzinę i nadal rejestrować nieprawidłowy wynik, ponieważ przyczyną problemu nie jest asynchronizacja.
HMR
127

Dodatkowe szczegóły do poprzedniej odpowiedzi :

Chociaż React setState jest asynchroniczny (zarówno klasy, jak i hooki) i kuszące jest wykorzystanie tego faktu do wyjaśnienia obserwowanego zachowania, nie jest to powód, dla którego tak się dzieje.

TLDR: Przyczyną jest zamknięcie zakresu wokół niezmiennej constwartości.


Rozwiązania:

  • odczytaj wartość w funkcji renderowania (nie wewnątrz funkcji zagnieżdżonych):

    useEffect(() => { setMovies(result) }, [])
    console.log(movies)
  • dodaj zmienną do zależności (i użyj reguły React -hooks / wyczerpująco-deps eslint):

    useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
  • użyj zmiennego odniesienia (gdy powyższe nie jest możliwe):

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

Wyjaśnienie, dlaczego tak się dzieje:

Gdyby async był jedynym powodem, byłoby to możliwe await setState() .

Jednak oba propsi stateniezmienne podczas 1 renderowania .

Traktuj this.statetak, jakby było niezmienne.

W przypadku haków to założenie jest wzmocnione przez użycie stałych wartości ze constsłowem kluczowym:

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

Wartość może być różna w 2 renderach, ale pozostaje stała wewnątrz samego renderowania i wewnątrz wszelkich zamknięć (funkcji, które działają dłużej nawet po zakończeniu renderowania, np. useEffectProgramy obsługi zdarzeń, wewnątrz dowolnego Promise lub setTimeout).

Rozważ następującą fałszywą, ale synchroniczną implementację podobną do React:

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

Aprillion
źródło
12
Doskonała odpowiedź. IMO, ta odpowiedź pozwoli Ci zaoszczędzić najwięcej zawodu w przyszłości, jeśli zainwestujesz wystarczająco dużo czasu, aby ją przeczytać i przyswoić.
Scott Mallon
To sprawia, że ​​bardzo trudno jest używać kontekstu w połączeniu z routerem, tak? Gdy przechodzę do następnej strony, stan zniknął .. I nie mogę ustawićState tylko wewnątrz hooków useEffect, ponieważ nie mogą renderować ... Doceniam kompletność odpowiedzi i wyjaśnia to, co widzę, ale ja nie mogę wymyślić, jak to pokonać. Przekazuję funkcje w kontekście, aby zaktualizować wartości, które następnie nie są skutecznie utrwalane ... Więc muszę je wyciągnąć z zamknięcia w kontekście, tak?
Al Joslin
@AlJoslin na pierwszy rzut oka wydaje się być osobnym problemem, nawet jeśli może to być spowodowane zakresem zamknięcia. Jeśli masz konkretne pytanie, utwórz nowe pytanie z
przepełnieniem stosu
1
w rzeczywistości właśnie skończyłem przepisywanie za pomocą useReducer, po artykule @kentcdobs (ref poniżej), który naprawdę dał mi solidny wynik, który nie cierpi ani trochę z powodu problemów z zamykaniem. (ref: kentcdodds.com/blog/how-to-use-react-context-effectively )
Al Joslin,
1

Właśnie skończyłem przepisywanie z useReducer, po artykule @kentcdobs (ref poniżej), który naprawdę dał mi solidny wynik, który ani trochę nie cierpi z powodu tych problemów z zamykaniem.

widzieć: https://kentcdodds.com/blog/how-to-use-react-context-effectively

Skondensowałem jego czytelny szablon do mojego preferowanego poziomu SUCHOŚCI - przeczytanie jego implementacji w piaskownicy pokaże ci, jak to naprawdę działa.

Ciesz się, wiem, że jestem !!

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

o zastosowaniu podobnym do tego:

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

Teraz wszystko działa płynnie na wszystkich moich stronach

Miły!

Dzięki, Kent!

Al Joslin
źródło
-2
// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;

Teraz powinieneś zobaczyć, że Twój kod faktycznie wykonuje pracę. To, co nie działa, to console.log(movies). To dlatego, że movieswskazuje na stary stan . Jeśli przesuniesz swoją console.log(movies)zewnętrzną stronę useEffect, tuż nad zwrotem, zobaczysz zaktualizowany obiekt filmów.

Nikita Malyschkin
źródło