Метод useState не сразу отражает изменение


226

Я пытаюсь выучить хуки, и этот useStateметод меня запутал. Я присваиваю начальное значение состоянию в виде массива. У useStateменя метод set не работает даже с spread(...)или without spread operator. Я создал API на другом ПК, который я вызываю и получаю данные, которые я хочу установить в состояние.

Вот мой код:

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

Как setMovies(result)и setMovies(...result)не работает. Здесь нужна помощь.

Я ожидаю, что переменная результата будет помещена в массив фильмов.

Ответы:


294

Подобно setState в компонентах класса, созданных путем расширения React.Componentили React.PureComponent, обновление состояния с помощью средства обновления, предоставляемого useStateловушкой, также является асинхронным и не будет отражено немедленно.

Кроме того, основная проблема здесь не только в асинхронном характере, но и в том, что значения состояния используются функциями на основе их текущих закрытий, а обновления состояния будут отражаться в следующем повторном рендеринге, при котором существующие закрытия не затрагиваются, а новые созданы . Теперь в текущем состоянии значения внутри хуков получаются существующими замыканиями, а когда происходит повторный рендеринг, замыкания обновляются в зависимости от того, воссоздана ли функция снова или нет.

Даже если вы добавите setTimeoutфункцию, хотя тайм-аут сработает через некоторое время, когда setTimeoutпроизойдет повторная визуализация, функция все равно будет использовать значение из своего предыдущего закрытия, а не обновленное.

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

Если вы хотите выполнить действие при обновлении состояния, вам нужно использовать ловушку useEffect, как и componentDidUpdateв компонентах класса, поскольку сеттер, возвращаемый useState, не имеет шаблона обратного вызова

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

Что касается синтаксиса для обновления состояния, setMovies(result)предыдущее moviesзначение в состоянии будет заменено на значения, доступные из асинхронного запроса.

Однако, если вы хотите объединить ответ с ранее существовавшими значениями, вы должны использовать синтаксис обратного вызова обновления состояния вместе с правильным использованием синтаксиса распространения, например

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

23
Привет, а как насчет вызова useState внутри обработчика отправки формы? Я работаю над проверкой сложной формы, и я вызываю внутри submitHandler ловушки useState, и, к сожалению, изменения происходят не сразу!
da45

3
useEffectВозможно, это не лучшее решение, поскольку оно не поддерживает асинхронные вызовы. Итак, если мы хотим выполнить некоторую асинхронную проверку moviesизменения состояния, мы не можем это контролировать.
РА.

4
обратите внимание, что хотя совет очень хорош, объяснение причины может быть улучшено - ничего общего с тем фактом, the updater provided by useState hook является ли он асинхронным, в отличие от того, this.stateчто могло быть изменено, если бы this.setStateбыло синхронно, Замыкание вокруг const moviesостанется таким же, даже если useStateобеспечил синхронную функцию - см. пример в моем ответе
апрель

setMovies(prevMovies => ([...prevMovies, ...result]));работал у меня
Михир

1
Он регистрирует неправильный результат, потому что вы регистрируете устаревшее закрытие, а не потому, что сеттер является асинхронным. Если проблема была в асинхронном режиме, вы могли бы войти после тайм-аута, но вы можете установить тайм-аут на час и по-прежнему регистрировать неверный результат, потому что асинхронный режим не является причиной проблемы.
HMR

169

Дополнительные детали к предыдущему ответу :

Хотя РЕАКТ setState является асинхронной (оба класса и крючки), и это заманчиво использовать этот факт для объяснения наблюдаемого поведения, это не причина , почему это происходит.

TL; DR: причина в закрытии области вокруг неизменяемого constзначения.


Решения:

  • прочтите значение в функции рендеринга (не внутри вложенных функций):

    useEffect(() => { setMovies(result) }, [])
    console.log(movies)
    
  • добавить переменную в зависимости (и использовать правило eslint react -hooks / excustive -deps ):

    useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
    
  • используйте изменяемую ссылку (когда это невозможно):

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

Объяснение, почему это происходит:

Если бы асинхронность была единственной причиной, это было бы возможно await setState().

Howerver предполагается, что оба propsи stateне меняются во время 1 рендеринга .

Относитесь this.stateк нему как к неизменному.

С помощью хуков это предположение усиливается за счет использования постоянных значений с constключевым словом:

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

Значение может отличаться для двух отрисовок, но остается постоянным внутри самого отрисовки и внутри любых замыканий (функции, которые живут дольше даже после завершения отрисовки, например useEffect, обработчики событий, внутри любого Promise или setTimeout).

Рассмотрим следующую фальшивую, но синхронную реализацию , похожую на 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>


16
Отличный ответ. ИМО, этот ответ убережет вас от горя в будущем, если вы потратите достаточно времени, чтобы прочитать и усвоить его.
Скотт Мэллон,

1
Из-за этого очень сложно использовать контекст вместе с маршрутизатором, да? Когда я перехожу на следующую страницу, состояние исчезает ... И я не могу установитьState только внутри хуков useEffect, поскольку они не могут отображаться ... Я ценю полноту ответа, и он объясняет то, что я вижу, но я не могу понять, как это победить. Я передаю функции в контексте для обновления значений, которые затем не сохраняются эффективно ... Значит, я должен вывести их из-за закрытия в контексте, да?
Эл Джослин,

1
на самом деле я только что закончил перезапись с помощью useReducer, следуя статье @kentcdobs (см. ниже), которая действительно дала мне твердый результат, который не страдает от этих проблем с закрытием. (ref: kentcdodds.com/blog/how-to-use-react-context-effectively )
Эл Джослин,

1
Спасибо за отличный ответ - удалось прояснить, как работают замыкания, что часто упускается из виду среди разработчиков.
devsaw

1
@ACV Решение 2 отлично подходит для исходного вопроса. Если вам нужно решить другую проблему, YMMW, но я все еще на 100% уверен, что приведенный код работает так, как задокументировано, и проблема где-то в другом месте.
Апрель,

4

Я только что закончил перезапись с помощью useReducer, следуя статье @kentcdobs (см. Ниже), которая действительно дала мне твердый результат, который не страдает от этих проблем с закрытием.

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

Я сжал его читаемый шаблон до моего предпочтительного уровня СУХОСТИ - чтение его реализации в песочнице покажет вам, как это на самом деле работает.

Наслаждайтесь, я знаю !!

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

с использованием, подобным этому:

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

Теперь все плавно сохраняется на всех моих страницах

Ницца!

Спасибо, Кент!


0

useEffect имеет собственное состояние / жизненный цикл, он не будет обновляться, пока вы не передадите функцию в параметрах или эффект не будет уничтожен.

объект и массив распространение или отдых не будут работать внутри useEffect.

React.useEffect(() => {
    console.log("effect");
    (async () => {
        try {
            let result = await fetch("/query/countries");
            const res = await result.json();
            let result1 = await fetch("/query/projects");
            const res1 = await result1.json();
            let result11 = await fetch("/query/regions");
            const res11 = await result11.json();
            setData({
                countries: res,
                projects: res1,
                regions: res11
            });
        } catch {}
    })(data)
}, [setData])
# or use this
useEffect(() => {
    (async () => {
        try {
            await Promise.all([
                fetch("/query/countries").then((response) => response.json()),
                fetch("/query/projects").then((response) => response.json()),
                fetch("/query/regions").then((response) => response.json())
            ]).then(([country, project, region]) => {
                // console.log(country, project, region);
                setData({
                    countries: country,
                    projects: project,
                    regions: region
                });
            })
        } catch {
            console.log("data fetch error")
        }
    })()
}, [setData]);

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

Теперь вы должны увидеть, что ваш код на самом деле делает работу. Что не работает, так это console.log(movies). Это потому, что moviesуказывает на старое состояние . Если вы переместите свой console.log(movies)за пределы useEffect, прямо над возвратом, вы увидите обновленный объект фильмов.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.