Метод setState set не відображає зміни відразу


168

Я намагаюся навчитися гачкам, і useStateметод мене збентежив. Я присвоюю початкове значення стану у вигляді масиву. Встановлений метод в useStateмені не працює навіть з 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)не працює. Тут можна скористатись деякою допомогою.

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

Відповіді:


220

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

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

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

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

17
Привіт, що з викликом useState всередині обробника форми подання форми? Я працюю над валідацією складної форми, і я закликаю всередині submitHandler useState гачки і, на жаль, зміни не є негайними!
da45

2
useEffectможливо, це не найкраще рішення, оскільки він не підтримує асинхронні дзвінки. Отже, якщо ми хотіли б зробити деяку асинхронну перевірку щодо moviesзміни стану, ми не маємо над цим контролю.
РА.

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

setMovies(prevMovies => ([...prevMovies, ...result]));працював на мене
Mihir

Це реєстрація неправильного результату, тому що ви реєструєте несвіжую закриття не тому, що сеттер асинхронний. Якщо проблема була в системі async, ви можете ввійти через тайм-аут, але ви можете встановити тайм-аут на годину і все ще записати неправильний результат, оскільки async - це не те, що викликає проблему.
HMR

127

Додаткові відомості до попередньої відповіді :

Хоча React setState є асинхронним (і класи, і гачки), і цей факт спокусливо використовувати для пояснення спостережуваної поведінки, це не є причиною, чому це відбувається.

TLDR: Причина - це область закриття навколо незмінного constзначення.


Рішення:

  • читати значення у функції візуалізації (не всередині вкладених функцій):

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

    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, обробники подій, всередині будь-якого обіцяння або setTimeout).

Розглянемо наступну підроблену, але синхронну реалізацію, схожу на реалізацію:

// 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
Відмінна відповідь. ІМО, ця відповідь врятує вам найбільше серце в майбутньому, якщо ви вкладете достатньо часу, щоб його прочитати та засвоїти.
Скотт Маллон

Це робить дуже важко використовувати контекст спільно з маршрутизатором, так? Коли я переходжу до наступної сторінки, стан пропав .. І я не можу встановитидержаву лише всередині використанняEffect гачки, оскільки вони не можуть винести ... Я ціную повноту відповіді, і це пояснює те, що я бачу, але я не можу зрозуміти, як це перемогти. Я передаю функції в контексті для оновлення значень, які потім не зберігаються ефективно ... Тож я маю вивести їх із закриття в контексті, так?
Аль Джослін

@AlJoslin на перший погляд, це здається окремою проблемою, навіть якщо це може бути викликано сферою закриття. Якщо у вас є конкретне питання, будь ласка, створіть нове запитання про
stackoverflow

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

1

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

дивіться: https://kentcdodds.com/blog/how-to-use-react-context-effely

Я ущільнив його читабельну плиту до бажаного рівня DRYness - прочитавши його реалізацію пісочниці, ви покажете, як вона насправді працює.

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

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

Тепер все гладко зберігається скрізь на всіх моїх сторінках

Приємно!

Дякую Кент!


-2
// 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.