O método set useState não reflete a alteração imediatamente

167

Estou tentando aprender ganchos e o useStatemétodo me deixou confusa. Estou atribuindo um valor inicial a um estado na forma de uma matriz. O método set in useStatenão está funcionando para mim, mesmo com spread(...)ou without spread operator. Eu criei uma API em outro PC que estou chamando e buscando os dados que desejo definir no estado.

Aqui está o meu 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);

O setMovies(result)bem como setMovies(...result)não está funcionando. Poderia usar alguma ajuda aqui.

Espero que a variável de resultado seja inserida na matriz de filmes.

Pranjal
fonte

Respostas:

219

Muito parecido com os componentes setState in Class criados pela extensão React.Componentou React.PureComponentatualização do estado usando o atualizador fornecido pelouseState gancho também é assíncrona e não refletirá imediatamente

Também a questão principal aqui, não apenas a natureza assíncrona, mas o fato de que os valores de estado são usados ​​por funções com base em seus fechamentos atuais e atualizações de estado refletirão na próxima nova renderização, pela qual os fechamentos existentes não são afetados, mas novos são criados . Agora, no estado atual, os valores dentro dos ganchos são obtidos pelos fechamentos existentes e, quando ocorre uma nova renderização, os fechamentos são atualizados com base no fato de a função ser recriada novamente ou não

Mesmo se você adicionar uma setTimeoutfunção, o tempo limite será executado após algum tempo pelo qual a nova renderização teria acontecido, mas o setTimout ainda usará o valor do seu fechamento anterior e não o atualizado.

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

Se você deseja executar uma ação na atualização de estado, é necessário usar o gancho useEffect, como componentDidUpdateem componentes de classe, pois o setter retornado por useState não possui um padrão de retorno de chamada

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

No que diz respeito à sintaxe para atualizar o estado, setMovies(result)substituirá o moviesvalor anterior no estado pelos valores disponíveis na solicitação assíncrona

No entanto, se você deseja mesclar a resposta com os valores existentes anteriormente, você deve usar a sintaxe de retorno de chamada da atualização de estado junto com o uso correto da sintaxe de propagação, como

setMovies(prevMovies => ([...prevMovies, ...result]));
Shubham Khatri
fonte
17
Olá, que tal chamar useState dentro de um manipulador de envio de formulários? Estou trabalhando na validação de um formulário complexo e chamo dentro de hooks submitHandler useState e, infelizmente, as alterações não são imediatas!
da45 19/04/19
2
useEffecttalvez não seja a melhor solução, pois não suporta chamadas assíncronas. Portanto, se gostaríamos de fazer alguma validação assíncrona sobre a moviesmudança de estado, não temos controle sobre ela.
RA.
2
observe que, embora o conselho seja muito bom, a explicação da causa pode ser aprimorada - nada a ver com o fato de the updater provided by useState hook ser assíncrono ou não , ao contrário do this.stateque poderia ter sido alterado se this.setStatefosse síncrono, o fechamento em torno const moviespermaneceria o mesmo, mesmo que useStateforneceu uma função síncrona - veja o exemplo na minha resposta
Abr 20/11/19
setMovies(prevMovies => ([...prevMovies, ...result]));trabalhou para mim
Mihir
Ele está registrando o resultado errado porque você está registrando um fechamento obsoleto, não porque o setter é assíncrono. Se o problema de async fosse o problema, você poderia registrar-se após um tempo limite, mas poderia definir um tempo limite por uma hora e ainda registrar o resultado errado, pois não é o que está causando o problema.
HMR
126

Detalhes adicionais para a resposta anterior :

Embora o React setState seja assíncrono (tanto de classe quanto de gancho), e é tentador usar esse fato para explicar o comportamento observado, não é a razão pela qual isso acontece.

TLDR: O motivo é um escopo de fechamento em torno de um constvalor imutável .


Soluções:

  • leia o valor na função render (não dentro das funções aninhadas):

    useEffect(() => { setMovies(result) }, [])
    console.log(movies)
  • adicione a variável nas dependências (e use a regra react -hooks / exaustive-deps eslint):

    useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
  • use uma referência mutável (quando o acima não for possível):

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

Explicação por que isso acontece:

Se o assíncrono fosse o único motivo, seria possível await setState().

Howerver, ambos propse statesão assumidos como inalteráveis ​​durante 1 render .

Trate this.statecomo se fosse imutável.

Com ganchos, essa suposição é aprimorada usando valores constantes com a constpalavra-chave:

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

O valor pode ser diferente entre 2 renderizações, mas permanece uma constante dentro da renderização em si e dentro de quaisquer fechamentos (funções que permanecem mais longas mesmo após a conclusão da renderização, por exemplo,useEffect , manipuladores de eventos, dentro de qualquer Promise ou setTimeout).

Considere a seguinte implementação falsa, mas síncrona , do tipo 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
fonte
11
Excelente resposta. Na IMO, essa resposta poupará mais desgosto no futuro se você investir tempo suficiente para ler e absorvê-lo.
Scott Mallon 20/01
Isso torna muito difícil usar um contexto em conjunto com um roteador, sim? À medida que chego à próxima página, o estado se foi. E não posso definir o Estado apenas dentro dos ganchos useEffect, pois eles não podem renderizar. Aprecio a totalidade da resposta e ela explica o que estou vendo, mas eu não consigo descobrir como vencê-lo. Estou passando funções no contexto para atualizar valores que então não persistem efetivamente ... Então, tenho que tirá-los do fechamento no contexto, sim?
Al Joslin
O @AlJoslin, à primeira vista, parece um problema separado, mesmo que possa ser causado pelo escopo do fechamento. Se você tiver uma pergunta concreta, crie uma nova pergunta de stackoverflow com exemplo de código e tudo ...
Aprillion
1
na verdade, acabei de reescrever com useReducer, seguindo o artigo @kentcdobs (ref abaixo), que realmente me deu um resultado sólido que não sofre nem um pouco com esses problemas de fechamento. (ref: kentcdodds.com/blog/how-to-use-react-context-effectively )
Al Joslin
1

Acabei de reescrever com useReducer, seguindo o artigo @kentcdobs (ref abaixo), que realmente me deu um resultado sólido que não sofre nem um pouco com esses problemas de fechamento.

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

Condensei seu clichê legível para o meu nível preferido de DRYness - ler a implementação do sandbox mostrará como ele realmente funciona.

Aproveite, eu sei que sou !!

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

com um uso semelhante 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 = {};

Agora tudo persiste sem problemas em todos os lugares em todas as minhas páginas

Agradável!

Graças Kent!

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

Agora você deve ver, que o código realmente faz o trabalho. O que não funciona é o console.log(movies). Isso ocorre porque moviesaponta para o estado antigo . Se você se mudar para console.log(movies)fora useEffect, logo acima do retorno, verá o objeto de filmes atualizados.

Nikita Malyschkin
fonte