Die Methode useState set spiegelt Änderungen nicht sofort wider

162

Ich versuche Hooks zu lernen und die useStateMethode hat mich verwirrt. Ich weise einem Zustand in Form eines Arrays einen Anfangswert zu. Die eingestellte Methode in useStatefunktioniert bei mir nicht einmal mit spread(...)oder without spread operator. Ich habe eine API auf einem anderen PC erstellt, den ich aufrufe, und die Daten abgerufen, die ich in den Status versetzen möchte.

Hier ist mein Code:

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

Das funktioniert setMovies(result)genauso gut wie setMovies(...result)nicht. Könnte hier Hilfe gebrauchen.

Ich erwarte, dass die Ergebnisvariable in das Filmarray verschoben wird.

Pranjal
quelle

Antworten:

215

Ähnlich wie bei setState in Class-Komponenten, die durch Erweitern React.Componentoder erstellt wurden React.PureComponent, useStateist auch die Statusaktualisierung mit dem von hook bereitgestellten Updater asynchron und wird nicht sofort wiedergegeben

Auch das Hauptproblem hierbei ist nicht nur die asynchrone Natur, sondern auch die Tatsache, dass Statuswerte von Funktionen basierend auf ihren aktuellen Abschlüssen und Statusaktualisierungen verwendet werden. Dies wird beim nächsten erneuten Rendern berücksichtigt, bei dem die vorhandenen Abschlüsse nicht betroffen sind, sondern neue erstellt werden . Im aktuellen Status werden die Werte in Hooks von vorhandenen Closures abgerufen. Wenn ein erneutes Rendern erfolgt, werden die Closures basierend darauf aktualisiert, ob die Funktion erneut erstellt wird oder nicht

Selbst wenn Sie eine setTimeoutFunktion hinzufügen , wird das Timeout zwar nach einiger Zeit ausgeführt, bis zu der das erneute Rendern stattgefunden hätte, aber setTimout verwendet weiterhin den Wert aus dem vorherigen Abschluss und nicht den aktualisierten.

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

Wenn Sie eine Aktion für die Statusaktualisierung ausführen möchten, müssen Sie den useEffect-Hook verwenden, ähnlich wie bei der Verwendung componentDidUpdatein Klassenkomponenten, da der von useState zurückgegebene Setter kein Rückrufmuster hat

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

In Bezug auf die Syntax zum Aktualisieren des Status setMovies(result)wird der vorherige moviesWert im Status durch den Wert ersetzt, der in der asynchronen Anforderung verfügbar ist

Wenn Sie jedoch die Antwort mit den zuvor vorhandenen Werten zusammenführen möchten, müssen Sie die Rückrufsyntax der Statusaktualisierung zusammen mit der korrekten Verwendung der Spread-Syntax wie verwenden

setMovies(prevMovies => ([...prevMovies, ...result]));
Shubham Khatri
quelle
16
Hallo, was ist mit dem Aufruf von useState in einem Formularübermittlungs-Handler? Ich arbeite an der Validierung eines komplexen Formulars und rufe in submitHandler useState-Hooks auf. Leider sind Änderungen nicht sofort möglich!
da45
2
useEffectMöglicherweise ist dies jedoch nicht die beste Lösung, da asynchrone Aufrufe nicht unterstützt werden. Wenn wir also eine asynchrone Validierung bei Statusänderungen durchführen möchten movies, haben wir keine Kontrolle darüber.
RA.
2
Bitte beachten Sie, dass der Rat zwar sehr gut ist, die Erklärung der Ursache jedoch verbessert werden kann - nichts damit zu tun, ob er the updater provided by useState hook asynchron ist oder nicht , im Gegensatz zu dem this.state, der hätte mutiert werden können, wenn er this.setStatesynchron gewesen wäre, würde die Schließung auch const moviesdann gleich bleiben, wenn useStateeine synchrone Funktion bereitgestellt - siehe das Beispiel in meiner Antwort
Aprillion
setMovies(prevMovies => ([...prevMovies, ...result]));arbeitete für mich
Mihir
Es wird das falsche Ergebnis protokolliert, weil Sie einen veralteten Abschluss protokollieren, nicht weil der Setter asynchron ist. Wenn Async das Problem war, konnten Sie sich nach einer Zeitüberschreitung anmelden, aber Sie konnten eine Zeitüberschreitung für eine Stunde festlegen und trotzdem das falsche Ergebnis protokollieren, da Async nicht die Ursache des Problems ist.
HMR
126

Zusätzliche Details zur vorherigen Antwort :

Während Reagieren der setState ist asynchron (beide Klassen und Haken), und es ist verlockend , diese Tatsache zu verwenden , um das beobachtete Verhalten zu erklären, ist es nicht der Grund , warum es passiert.

TLDR: Der Grund ist ein Schließungsbereich um einen unveränderlichen constWert.


Lösungen:

  • Lesen Sie den Wert in der Renderfunktion (nicht in verschachtelten Funktionen):

    useEffect(() => { setMovies(result) }, [])
    console.log(movies)
  • Fügen Sie die Variable in Abhängigkeiten ein (und verwenden Sie die Eslint-Regel " react-hooks / erschöpfende-deps "):

    useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
  • Verwenden Sie eine veränderbare Referenz (wenn dies nicht möglich ist):

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

Erklärung, warum es passiert:

Wenn Async der einzige Grund wäre, wäre dies möglich await setState().

Es wird jedoch davon ausgegangen, dass beide propsund während eines Renderns unverändert sind .state

Behandle, this.stateals ob es unveränderlich wäre.

Bei Hooks wird diese Annahme durch die Verwendung konstanter Werte mit dem constSchlüsselwort erweitert:

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

Der Wert kann zwischen zwei Renderings unterschiedlich sein, bleibt jedoch innerhalb des Renderings selbst und innerhalb aller Schließungen konstant (Funktionen, die auch nach Abschluss des useEffectRenderns länger gültig sind , z. B. Ereignishandler, innerhalb eines Promise oder setTimeout).

Betrachten Sie die folgende gefälschte, aber synchrone , reaktionsähnliche Implementierung:

// 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
quelle
11
Hervorragende Antwort. IMO, diese Antwort erspart Ihnen in Zukunft den größten Herzschmerz, wenn Sie genügend Zeit investieren, um sie durchzulesen und zu absorbieren.
Scott Mallon
Dies macht es sehr schwierig, einen Kontext in Verbindung mit einem Router zu verwenden, ja? Als ich zur nächsten Seite komme, ist der Status verschwunden. Und ich kann State nicht nur innerhalb von useEffect-Hooks setzen, da sie nicht gerendert werden können. Ich schätze die Vollständigkeit der Antwort und sie erklärt, was ich sehe, aber ich Ich kann nicht herausfinden, wie ich es schlagen soll. Ich übergebe Funktionen im Kontext, um Werte zu aktualisieren, die dann nicht effektiv bestehen bleiben ... Also muss ich sie aus dem Abschluss im Kontext herausholen, ja?
Al Joslin
@AlJoslin Auf den ersten Blick scheint dies ein separates Problem zu sein, auch wenn es möglicherweise durch den Schließungsbereich verursacht wird. Wenn Sie eine konkrete Frage haben, erstellen Sie bitte eine neue Stackoverflow-Frage mit Codebeispiel und allem ...
Aprillion
1
Eigentlich habe ich gerade ein Umschreiben mit useReducer abgeschlossen, das dem Artikel @kentcdobs (siehe unten) folgt, der mir wirklich ein solides Ergebnis gebracht hat, das nicht ein bisschen unter diesen Schließungsproblemen leidet. (Ref: kentcdodds.com/blog/how-to-use-react-context-effectively )
Al Joslin
1

Ich habe gerade eine Umschreibung mit useReducer abgeschlossen, die dem Artikel @kentcdobs (siehe unten) folgt und mir wirklich ein solides Ergebnis liefert, das nicht ein bisschen unter diesen Schließungsproblemen leidet.

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

Ich habe seine lesbare Boilerplate auf mein bevorzugtes Maß an Trockenheit verdichtet - das Lesen seiner Sandbox-Implementierung zeigt Ihnen, wie es tatsächlich funktioniert.

Viel Spaß, ich weiß ich bin !!

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

mit einer ähnlichen Verwendung:

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

Jetzt bleibt alles auf allen meinen Seiten reibungslos

Nett!

Danke Kent!

Al Joslin
quelle
-1
// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;

Jetzt sollten Sie sehen, dass Ihr Code tatsächlich funktioniert . Was nicht funktioniert ist das console.log(movies). Dies liegt daran, dass moviesauf den alten Zustand hingewiesen wird . Wenn Sie Ihre console.log(movies)Außenseite useEffectdirekt über der Rückgabe verschieben, wird das aktualisierte Filmobjekt angezeigt.

Nikita Malyschkin
quelle