Il metodo set useState non riflette immediatamente la modifica


168

Sto cercando di imparare gli hook e il useStatemetodo mi ha confuso. Sto assegnando un valore iniziale a uno stato sotto forma di un array. Il metodo set in useStatenon funziona anche con spread(...)o without spread operator. Ho creato un'API su un altro PC che sto chiamando e recuperando i dati che voglio impostare nello stato.

Ecco il mio codice:

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

Il setMovies(result)così come setMovies(...result)non funziona. Potrei usare un po 'di aiuto qui.

Mi aspetto che la variabile del risultato venga inserita nell'array dei filmati.

Risposte:


221

Proprio come i componenti setState in Class creati estendendo React.Componento React.PureComponent, anche l'aggiornamento dello stato utilizzando il programma di aggiornamento fornito da useStatehook è asincrono e non rifletterà immediatamente

Anche qui il problema principale non è solo la natura asincrona, ma il fatto che i valori di stato vengano utilizzati dalle funzioni in base alle loro chiusure correnti e gli aggiornamenti di stato si rifletteranno nel prossimo rendering successivo con il quale le chiusure esistenti non sono interessate ma ne vengono create di nuove . Ora nello stato corrente i valori all'interno degli hook sono ottenuti da chiusure esistenti e quando si verifica un nuovo rendering, le chiusure vengono aggiornate in base al fatto che la funzione venga ricreata o meno

Anche se si aggiunge una setTimeoutfunzione, anche se il timeout verrà eseguito dopo un certo periodo di tempo in cui si sarebbe verificato il nuovo rendering, ma setTimout utilizzerà comunque il valore della sua chiusura precedente e non quella aggiornata.

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

Se si desidera eseguire un'azione sull'aggiornamento dello stato, è necessario utilizzare l'hook useEffect, in modo molto simile all'uso dei componentDidUpdatecomponenti di classe poiché il setter restituito da useState non ha un modello di callback

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

Per quanto riguarda la sintassi per aggiornare lo stato, setMovies(result)sostituirà il moviesvalore precedente nello stato con quelli disponibili dalla richiesta asincrona

Tuttavia, se si desidera unire la risposta con i valori precedentemente esistenti, è necessario utilizzare la sintassi di richiamata dell'aggiornamento dello stato insieme all'uso corretto della sintassi diffusa come

setMovies(prevMovies => ([...prevMovies, ...result]));

17
Ciao, che ne dici di chiamare useState all'interno di un gestore di invio moduli? Sto lavorando alla convalida di un modulo complesso, e chiamo dentro submitHandler useState hook e purtroppo le modifiche non sono immediate!
da45,

2
useEffectpotrebbe non essere la soluzione migliore, poiché non supporta le chiamate asincrone. Pertanto, se desideriamo effettuare una convalida asincrona sul moviescambio di stato, non abbiamo alcun controllo su di esso.
RA.

2
si prega di notare che mentre il consiglio è molto buono, la spiegazione della causa può essere migliorata - niente a che fare con il fatto che sia o meno the updater provided by useState hook asincrono, a differenza this.stateche avrebbe potuto essere mutato se this.setStatefosse sincrono, la Chiusura intorno const moviesrimarrebbe la stessa anche se useStatefornito una funzione sincrona - vedi l'esempio nella mia risposta
Aprillion

setMovies(prevMovies => ([...prevMovies, ...result]));ha funzionato per me
Mihir il

Si sta registrando il risultato sbagliato perché si sta registrando una chiusura obsoleta non perché il setter è asincrono. Se il problema era asincrono, è possibile accedere dopo un timeout, ma è possibile impostare un timeout per un'ora e comunque registrare il risultato errato perché asincrono non è ciò che sta causando il problema.
HMR,

127

Dettagli aggiuntivi alla risposta precedente :

Mentre React setState è asincrono (sia classi che hook), ed è allettante utilizzare quel fatto per spiegare il comportamento osservato, non è il motivo per cui ciò accade.

TLDR: il motivo è un ambito di chiusura attorno a un constvalore immutabile .


soluzioni:

  • leggere il valore nella funzione di rendering (non all'interno delle funzioni nidificate):

    useEffect(() => { setMovies(result) }, [])
    console.log(movies)
  • aggiungi la variabile nelle dipendenze (e usa il reatt-hooks / esaustivo-deps regola eslint di reazioni esausti ):

    useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
  • utilizzare un riferimento modificabile (quando quanto sopra non è possibile):

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

Spiegazione perché succede:

Se la sola ragione fosse asincrona, sarebbe possibile await setState().

Howerver, entrambi propse statesono considerati immutabili durante 1 rendering .

Trattare this.state come se fosse immutabile.

Con gli hook, questa ipotesi è migliorata usando valori costanti con la constparola chiave:

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

Il valore potrebbe essere diverso tra 2 rendering, ma rimane una costante all'interno del rendering stesso e all'interno di eventuali chiusure (funzioni che vivono più a lungo anche al termine del rendering, ad esempio useEffectgestori di eventi, all'interno di Promise o setTimeout).

Prendi in considerazione la seguente implementazione falsa, ma sincrona , simile a 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>


12
Risposta eccellente. IMO, questa risposta ti farà risparmiare più dolore in futuro se investi abbastanza tempo per leggerla e assorbirla.
Scott Mallon,

Questo rende molto difficile usare un contesto insieme a un router, sì? Quando arrivo alla pagina successiva lo stato è sparito .. E non riesco a impostare lo stato solo all'interno dell'uso Ganci per effetti poiché non possono eseguire il rendering ... Apprezzo la completezza della risposta e spiega ciò che vedo, ma io non riesco a capire come batterlo. Sto passando funzioni nel contesto per aggiornare i valori che quindi non persistono in modo efficace ... Quindi devo farli uscire dalla chiusura nel contesto, sì?
Al Joslin,

@AlJoslin a prima vista, sembra un problema separato, anche se potrebbe essere causato dall'ambito di chiusura. Se hai una domanda concreta, crea una nuova domanda
stackoverflow

1
in realtà ho appena finito di riscrivere con useReducer, seguendo l'articolo di @kentcdobs (rif sotto) che mi ha dato un risultato solido che non soffre neanche un po 'di questi problemi di chiusura. (ref: kentcdodds.com/blog/how-to-use-react-context-effectiveively )
Al Joslin

1

Ho appena finito di riscrivere con useReducer, seguendo l'articolo di @kentcdobs (rif sotto) che mi ha dato un risultato solido che non soffre neanche un po 'di questi problemi di chiusura.

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

Ho condensato la sua piastra di lettura leggibile al mio livello preferito di ASCIUGATURA - leggere l'implementazione della sua sandbox ti mostrerà come funziona effettivamente.

Divertiti, lo so che lo sono !!

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

con un utilizzo simile a questo:

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

Ora tutto persiste senza problemi ovunque in tutte le mie pagine

Bello!

Grazie Kent!


-2
// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;

Ora dovresti vedere che il tuo codice funziona davvero . Ciò che non funziona è il console.log(movies). Questo perché moviesindica il vecchio stato . Se sposti l' console.log(movies)esterno di useEffect, proprio sopra il ritorno, vedrai l'oggetto film aggiornato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.