El método setState set no refleja el cambio inmediatamente

167

Estoy tratando de aprender ganchos y el useStatemétodo me ha confundido. Estoy asignando un valor inicial a un estado en forma de matriz. El método set en useStateno me funciona incluso con spread(...)o without spread operator. He hecho una API en otra PC a la que estoy llamando y obteniendo los datos que quiero establecer en el estado.

Aquí está mi código:

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

El setMovies(result)bien como setMovies(...result)no funciona. Podría usar algo de ayuda aquí.

Espero que la variable de resultado se inserte en la matriz de películas.

Pranjal
fuente

Respuestas:

219

Al igual que los componentes de setState in Class creados al extender React.Componento React.PureComponent, la actualización de estado utilizando el actualizador proporcionado por useStatehook también es asíncrona y no reflejará de inmediato

Además, el problema principal aquí no es solo la naturaleza asincrónica sino el hecho de que los valores de estado son utilizados por las funciones en función de sus cierres actuales y las actualizaciones de estado se reflejarán en la próxima representación por la cual los cierres existentes no se ven afectados sino que se crean otros nuevos . Ahora, en el estado actual, los valores dentro de los ganchos se obtienen por los cierres existentes y cuando ocurre una nueva representación, los cierres se actualizan en función de si la función se vuelve a crear o no.

Incluso si agrega una setTimeoutfunción, aunque el tiempo de espera se ejecutará después de un tiempo durante el cual la re-renderización habría sucedido, pero setTimout todavía usará el valor de su cierre anterior y no el actualizado.

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

Si desea realizar una acción en la actualización de estado, debe usar el enlace useEffect, al igual que componentDidUpdateen componentes de clase, ya que el setter devuelto por useState no tiene un patrón de devolución de llamada

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

En lo que respecta a la sintaxis para actualizar el estado, setMovies(result)reemplazará el moviesvalor anterior en el estado con los disponibles en la solicitud asíncrona

Sin embargo, si desea fusionar la respuesta con los valores existentes anteriormente, debe usar la sintaxis de devolución de llamada de actualización de estado junto con el uso correcto de la sintaxis extendida como

setMovies(prevMovies => ([...prevMovies, ...result]));
Shubham Khatri
fuente
17
Hola, ¿qué hay de llamar a useState dentro de un controlador de envío de formularios? Estoy trabajando para validar un formulario complejo, y llamo a los ganchos de useState de submitHandler y desafortunadamente los cambios no son inmediatos.
da45
2
useEffectSin embargo, podría no ser la mejor solución, ya que no admite llamadas asincrónicas. Entonces, si quisiéramos hacer una validación asíncrona en el moviescambio de estado, no tenemos control sobre eso.
RA.
2
tenga en cuenta que si bien el consejo es muy bueno, la explicación de la causa puede mejorarse: nada que ver con el hecho de si the updater provided by useState hook es asíncrono o no , a diferencia de lo this.stateque podría haber sido mutado si this.setStatefuera sincrónico, el cierre const moviesseguiría siendo el mismo incluso si useStateproporcionó una función sincrónica - vea el ejemplo en mi respuesta
Aprillion
setMovies(prevMovies => ([...prevMovies, ...result]));trabajó para mí
Mihir
Está registrando el resultado incorrecto porque está registrando un cierre obsoleto, no porque el configurador sea asíncrono. Si el problema era asíncrono, entonces podría iniciar sesión después de un tiempo de espera, pero podría establecer un tiempo de espera de una hora y aún así registrar el resultado incorrecto porque asíncrono no es lo que está causando el problema.
HMR
126

Detalles adicionales a la respuesta anterior :

Si bien React's setState es asíncrono (tanto clases como ganchos), y es tentador usar ese hecho para explicar el comportamiento observado, no es la razón por la que sucede.

TLDR: La razón es un alcance de cierre alrededor de un constvalor inmutable .


Soluciones:

  • lea el valor en la función de representación (no dentro de las funciones anidadas):

    useEffect(() => { setMovies(result) }, [])
    console.log(movies)
  • agregue la variable en dependencias (y use la regla react-hooks / exhaustive-deps eslint):

    useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
  • use una referencia mutable (cuando lo anterior no es posible):

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

Explicación por qué sucede:

Si async fuera la única razón, sería posible await setState().

Howerver, ambos propsy statese supone que no cambian durante 1 render .

Trátelo this.statecomo si fuera inmutable.

Con ganchos, esta suposición se mejora mediante el uso de valores constantes con la constpalabra clave:

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

El valor puede ser diferente entre 2 renders, pero permanece constante dentro del render en sí y dentro de cualquier cierre (funciones que duran más, incluso después de que finaliza el render, por ejemplo useEffect, controladores de eventos, dentro de cualquier Promise o setTimeout).

Considere seguir una implementación falsa, pero sincrónica , similar 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>

Aprillion
fuente
11
Excelente respuesta En mi opinión, esta respuesta le ahorrará la mayor angustia en el futuro si invierte suficiente tiempo para leerlo y absorberlo.
Scott Mallon
Esto hace que sea muy difícil usar un contexto junto con un enrutador, ¿sí? Cuando llego a la página siguiente, el estado se ha ido ... Y no puedo establecer el estado solo dentro de los ganchos de uso de efectos, ya que no se pueden procesar ... Aprecio la integridad de la respuesta y explica lo que estoy viendo, pero No puedo entender cómo vencerlo. Estoy pasando funciones en el contexto para actualizar valores que luego no persisten de manera efectiva ... Entonces, tengo que sacarlos del cierre en el contexto, ¿sí?
Al Joslin
@AlJoslin a primera vista, parece un problema separado, incluso si puede ser causado por el alcance del cierre. Si tiene una pregunta concreta, cree una nueva pregunta de stackoverflow con código de ejemplo y todo ...
Aprillion
1
De hecho, acabo de terminar una reescritura con useReducer, siguiendo el artículo de @kentcdobs (referencia más abajo) que realmente me dio un resultado sólido que no sufre ni un poco de estos problemas de cierre. (ref: kentcdodds.com/blog/how-to-use-react-context-effectively )
Al Joslin
1

Acabo de terminar una reescritura con useReducer, siguiendo el artículo de @kentcdobs (referencia a continuación) que realmente me dio un resultado sólido que no sufre ni un poco de estos problemas de cierre.

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

Condensé su repetitivo legible a mi nivel preferido de SECADO: leer su implementación de sandbox le mostrará cómo funciona realmente.

Disfruta, sé que lo estoy !!

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 uso similar a este:

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

Ahora todo persiste sin problemas en todas partes en todas mis páginas

¡Agradable!

Gracias Kent!

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

Ahora debería ver, que su código en realidad hace el trabajo. Lo que no funciona es el console.log(movies). Esto se debe a que moviesapunta al estado anterior . Si mueve su console.log(movies)exterior useEffect, justo encima del retorno, verá el objeto de películas actualizado.

Nikita Malyschkin
fuente