useState, méthode définie ne reflétant pas immédiatement le changement

168

J'essaie d'apprendre les crochets et la useStateméthode m'a rendu confus. J'attribue une valeur initiale à un état sous la forme d'un tableau. La méthode set dans useStatene fonctionne pas pour moi même avec spread(...)ou without spread operator. J'ai créé une API sur un autre PC que j'appelle et récupère les données que je veux mettre dans l'état.

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

Le setMovies(result)ainsi que setMovies(...result)ne fonctionne pas. Pourrait utiliser de l'aide ici.

Je m'attends à ce que la variable de résultat soit poussée dans le tableau des films.

Pranjal
la source

Réponses:

221

Tout comme setState dans les composants de classe créés en étendant React.Componentou React.PureComponent, la mise à jour de l'état à l'aide du programme de mise à jour fourni par useStatehook est également asynchrone et ne reflétera pas immédiatement

De plus, le problème principal ici, non seulement la nature asynchrone, mais le fait que les valeurs d'état sont utilisées par les fonctions en fonction de leurs fermetures actuelles et des mises à jour d'état se refléteront dans le prochain rendu par lequel les fermetures existantes ne sont pas affectées mais de nouvelles sont créées . Maintenant, dans l'état actuel, les valeurs dans les crochets sont obtenues par les fermetures existantes et quand un nouveau rendu se produit, les fermetures sont mises à jour selon que la fonction est recréée à nouveau ou non

Même si vous ajoutez une setTimeoutfonction, bien que le délai d'expiration s'exécutera après un certain temps au bout duquel le nouveau rendu se serait produit, mais setTimout utilisera toujours la valeur de sa fermeture précédente et non celle mise à jour.

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

Si vous souhaitez effectuer une action sur la mise à jour de l'état, vous devez utiliser le hook useEffect, un peu comme l'utilisation componentDidUpdatede composants in class puisque le setter retourné par useState n'a pas de modèle de rappel

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

En ce qui concerne la syntaxe de mise à jour de l'état, setMovies(result)remplacera la moviesvaleur précédente de l'état par celles disponibles à partir de la requête asynchrone

Cependant, si vous souhaitez fusionner la réponse avec les valeurs existantes précédemment, vous devez utiliser la syntaxe de rappel de la mise à jour de l'état avec l'utilisation correcte de la syntaxe de propagation comme

setMovies(prevMovies => ([...prevMovies, ...result]));
Shubham Khatri
la source
17
Salut, qu'en est-il d'appeler useState dans un gestionnaire de soumission de formulaire? Je travaille sur la validation d'un formulaire complexe, et j'appelle à l'intérieur des hooks useState de submitHandler et malheureusement les changements ne sont pas immédiats!
du45
2
useEffectCe n'est peut-être pas la meilleure solution, car il ne prend pas en charge les appels asynchrones. Donc, si nous souhaitons effectuer une validation asynchrone sur le movieschangement d'état, nous n'avons aucun contrôle dessus.
RA.
2
veuillez noter que bien que le conseil soit très bon, l'explication de la cause peut être améliorée - rien à voir avec le fait qu'elle soit the updater provided by useState hook asynchrone ou non , contrairement à ce this.statequi aurait pu être muté si elle this.setStateétait synchrone, la fermeture autour const moviesresterait la même même si useStatefourni une fonction synchrone - voir l'exemple dans ma réponse
Aprillion
setMovies(prevMovies => ([...prevMovies, ...result]));travaillé pour moi
Mihir
Il enregistre le mauvais résultat parce que vous enregistrez une fermeture périmée et non pas parce que l'installateur est asynchrone. Si l'async était le problème, vous pouvez vous connecter après un délai d'expiration, mais vous pouvez définir un délai d'expiration pour une heure et toujours consigner le mauvais résultat car l'async n'est pas la cause du problème.
HMR
127

Détails supplémentaires à la réponse précédente :

Bien que React setState soit asynchrone (à la fois les classes et les hooks), et qu'il soit tentant d'utiliser ce fait pour expliquer le comportement observé, ce n'est pas la raison pour laquelle cela se produit.

TLDR: La raison est une portée de fermeture autour d'une constvaleur immuable .


Solutions:

  • lire la valeur dans la fonction de rendu (pas dans les fonctions imbriquées):

    useEffect(() => { setMovies(result) }, [])
    console.log(movies)
  • ajoutez la variable dans les dépendances (et utilisez la règle eslint react-hooks / exhaust-deps ):

    useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
  • utilisez une référence mutable (lorsque ce qui précède n'est pas possible):

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

Explication pourquoi cela se produit:

Si l'async était la seule raison, ce serait possible await setState().

Howerver, les deux propset statesont supposés être inchangés pendant 1 rendu .

Traitez this.statecomme s'il était immuable.

Avec les hooks, cette hypothèse est améliorée en utilisant des valeurs constantes avec le constmot - clé:

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

La valeur peut être différente entre 2 rendus, mais reste une constante à l'intérieur du rendu lui-même et à l'intérieur des fermetures (les fonctions qui vivent plus longtemps même après la fin du rendu, par exemple useEffect, les gestionnaires d'événements, à l'intérieur de n'importe quelle Promise ou setTimeout).

Pensez à suivre une implémentation fausse, mais synchrone , de type 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
la source
12
Excellente réponse. IMO, cette réponse vous fera économiser le plus de chagrin à l'avenir si vous investissez suffisamment de temps pour la lire et l'absorber.
Scott Mallon le
Cela rend très difficile l'utilisation d'un contexte en conjonction avec un routeur, n'est-ce pas? Lorsque j'arrive à la page suivante, l'état a disparu. Et je ne peux pas définirState uniquement à l'intérieur des hooks useEffect car ils ne peuvent pas rendre ... J'apprécie l'exhaustivité de la réponse et cela explique ce que je vois, mais je ne peut pas comprendre comment le battre. Je passe des fonctions dans le contexte pour mettre à jour des valeurs qui alors ne persistent pas efficacement ... Donc je dois les sortir de la fermeture dans le contexte, oui?
Al Joslin
@AlJoslin à première vue, cela semble être un problème distinct, même s'il peut être causé par la portée de la fermeture. Si vous avez une question concrète, veuillez créer une nouvelle question stackoverflow avec un exemple de code et tout ...
avril
1
en fait je viens de terminer une réécriture avec useReducer, à la suite de l'article @kentcdobs (ref ci-dessous) qui m'a vraiment donné un résultat solide qui ne souffre pas du tout de ces problèmes de fermeture. (réf: kentcdodds.com/blog/how-to-use-react-context-effectively )
Al Joslin
1

Je viens de terminer une réécriture avec useReducer, suite à l'article de @kentcdobs (réf ci-dessous) qui m'a vraiment donné un résultat solide qui ne souffre pas du tout de ces problèmes de fermeture.

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

J'ai condensé son passe-partout lisible à mon niveau préféré de DRYness - la lecture de son implémentation sandbox vous montrera comment cela fonctionne réellement.

Profitez, je sais que je suis !!

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

avec un usage similaire à celui-ci:

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

Maintenant, tout persiste partout sur toutes mes pages

Agréable!

Merci Kent!

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

Maintenant , vous devriez voir que votre code fait ne travail. Ce qui ne fonctionne pas, c'est le console.log(movies). C'est parce que moviespointe vers l' ancien état . Si vous déplacez votre console.log(movies)extérieur de useEffect, juste au-dessus du retour, vous verrez l'objet de films mis à jour.

Nikita Malyschkin
la source