phương thức setState không phản ánh sự thay đổi ngay lập tức


167

Tôi đang cố gắng học móc và useStatephương pháp đã làm tôi bối rối. Tôi đang gán một giá trị ban đầu cho một trạng thái dưới dạng một mảng. Phương thức thiết lập trong useStatekhông hoạt động đối với tôi ngay cả với spread(...)hoặc without spread operator. Tôi đã tạo một API trên một PC khác mà tôi đang gọi và tìm nạp dữ liệu mà tôi muốn đặt vào trạng thái.

Đây là mã của tôi:

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

Các setMovies(result)cũng như setMovies(...result)không hoạt động. Có thể sử dụng một số trợ giúp ở đây.

Tôi hy vọng biến kết quả sẽ được đẩy vào mảng phim.

Câu trả lời:


219

Giống như các thành phần setState trong Class được tạo bằng cách mở rộng React.Componenthoặc React.PureComponent, cập nhật trạng thái bằng trình cập nhật được cung cấp bởi useStatehook cũng không đồng bộ và sẽ không phản ánh ngay lập tức

Ngoài ra, vấn đề chính ở đây không chỉ là bản chất không đồng bộ mà thực tế là các giá trị trạng thái được sử dụng bởi các hàm dựa trên các lần đóng hiện tại của chúng và các cập nhật trạng thái sẽ phản ánh trong lần kết xuất lại tiếp theo mà các lần đóng hiện tại không bị ảnh hưởng mà các lần đóng mới được tạo . Bây giờ ở trạng thái hiện tại, các giá trị trong hook được lấy bởi các lần đóng hiện có và khi kết xuất lại xảy ra, các lần đóng được cập nhật dựa trên việc chức năng có được tạo lại lần nữa hay không

Ngay cả khi bạn thêm một setTimeoutchức năng, mặc dù thời gian chờ sẽ chạy sau một thời gian mà việc kết xuất lại sẽ xảy ra, nhưng setTimout vẫn sẽ sử dụng giá trị từ lần đóng trước đó và không phải là cập nhật.

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

Nếu bạn muốn thực hiện một hành động về cập nhật trạng thái, bạn cần sử dụng hook useEffect, giống như sử dụng componentDidUpdatetrong các thành phần lớp vì trình thiết lập được trả về bởi useState không có mẫu gọi lại

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

Theo cú pháp cập nhật trạng thái có liên quan, setMovies(result)sẽ thay thế moviesgiá trị trước đó trong trạng thái bằng trạng thái có sẵn từ yêu cầu async

Tuy nhiên, nếu bạn muốn hợp nhất phản hồi với các giá trị hiện có trước đó, bạn phải sử dụng cú pháp gọi lại của cập nhật trạng thái cùng với việc sử dụng đúng cú pháp lây lan như

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

17
Xin chào, những gì về việc gọi useState bên trong một trình xử lý biểu mẫu? Tôi đang làm việc để xác nhận một biểu mẫu phức tạp và tôi gọi bên trong hookHandler useState và không may thay đổi ngay lập tức!
da45

2
useEffectcó thể không phải là giải pháp tốt nhất, vì nó không hỗ trợ các cuộc gọi không đồng bộ. Vì vậy, nếu chúng tôi muốn thực hiện một số xác nhận không đồng bộ về moviesthay đổi trạng thái, chúng tôi không có quyền kiểm soát đối với nó.
RA.

2
xin lưu ý rằng mặc dù lời khuyên rất tốt, nhưng lời giải thích về nguyên nhân có thể được cải thiện - không liên quan gì đến thực tế the updater provided by useState hook là không đồng bộ hay không , không giống như this.stateđiều đó có thể bị thay đổi nếu this.setStatelà đồng bộ, đóng cửa xung quanh const moviessẽ vẫn như vậy ngay cả khi useStateđã cung cấp một chức năng đồng bộ - xem ví dụ trong câu trả lời của tôi
'19

setMovies(prevMovies => ([...prevMovies, ...result]));làm việc cho tôi
Mihir

Nó đang ghi lại kết quả sai vì bạn đang ghi nhật ký đóng cửa cũ không phải vì setter không đồng bộ. Nếu async là vấn đề thì bạn có thể đăng nhập sau khi hết thời gian, nhưng bạn có thể đặt thời gian chờ trong một giờ và vẫn đăng nhập kết quả sai vì async không phải là nguyên nhân gây ra sự cố.
HMR

126

Chi tiết bổ sung cho câu trả lời trước :

Mặc dù React setState không đồng bộ (cả lớp và móc), và thật hấp dẫn khi sử dụng thực tế đó để giải thích hành vi được quan sát, đó không phải là lý do tại sao nó xảy ra.

TLDR: Lý do là một phạm vi đóng xung quanh một constgiá trị bất biến .


Các giải pháp:

  • đọc giá trị trong hàm render (không nằm trong các hàm lồng nhau):

    useEffect(() => { setMovies(result) }, [])
    console.log(movies)
  • thêm biến vào phụ thuộc (và sử dụng quy tắc eslint phản ứng / hookive -deps-deps ):

    useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
  • sử dụng một tham chiếu có thể thay đổi (khi không thể ở trên):

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

Giải thích tại sao nó xảy ra:

Nếu async là lý do duy nhất, nó sẽ có thể await setState().

Howerver, cả hai propsstateđược coi là không thay đổi trong 1 kết xuất .

Đối xử this.statenhư thể nó là bất biến.

Với hook, giả định này được tăng cường bằng cách sử dụng các giá trị không đổi với consttừ khóa:

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

Giá trị có thể khác nhau giữa 2 kết xuất, nhưng vẫn là một hằng số bên trong chính kết xuất và bên trong bất kỳ bao đóng nào (các chức năng tồn tại lâu hơn ngay cả khi kết xuất hoàn tất, ví dụ: useEffecttrình xử lý sự kiện, bên trong bất kỳ Promise hoặc setTimeout nào).

Xem xét việc thực hiện giả mạo, nhưng đồng bộ , giống như 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>


11
Câu trả lời tuyệt vời. IMO, câu trả lời này sẽ giúp bạn tiết kiệm được nhiều đau khổ nhất trong tương lai nếu bạn đầu tư đủ thời gian để đọc qua và tiếp thu nó.
Scott Mallon

Điều này làm cho nó rất khó để sử dụng một bối cảnh kết hợp với bộ định tuyến, đúng không? Khi tôi đến trang tiếp theo, trạng thái đã biến mất .. Và tôi không thể thiết lậpState chỉ bên trong hook Sử dụng vì chúng không thể hiển thị ... Tôi đánh giá cao sự hoàn chỉnh của câu trả lời và nó giải thích những gì tôi đang thấy, nhưng tôi không thể tìm ra cách để đánh bại nó. Tôi đang chuyển các chức năng trong ngữ cảnh để cập nhật các giá trị mà sau đó không tồn tại hiệu quả ... Vì vậy, tôi phải đưa chúng ra khỏi sự đóng cửa trong bối cảnh, đúng không?
Al Joslin

@AlJoslin trong cái nhìn đầu tiên, đó có vẻ như là một vấn đề riêng biệt, ngay cả khi nó có thể được gây ra bởi phạm vi đóng. Nếu bạn có một câu hỏi cụ thể, hãy tạo một câu hỏi stackoverflow mới với mã ví dụ và tất cả các ...
Aprillion

1
thực sự tôi vừa hoàn thành việc viết lại với useReducer, sau bài viết của @kentcdobs (tham khảo bên dưới), điều này thực sự mang lại cho tôi một kết quả chắc chắn mà không phải chịu một chút nào từ những vấn đề đóng cửa này. (ref: kentcdodds.com/blog/how-to-use-react-context-effectively )
Al Joslin

1

Tôi vừa hoàn thành việc viết lại với useReducer, sau bài viết của @kentcdobs (tham khảo bên dưới), điều này thực sự mang lại cho tôi một kết quả chắc chắn mà không phải chịu một chút nào từ những vấn đề đóng cửa này.

xem: https://bestcdodds.com/blog/how-to-use-react-context-effectively

Tôi đã cô đọng cái nồi hơi có thể đọc được của anh ấy đến mức DRYness ưa thích của tôi - đọc triển khai hộp cát của anh ấy sẽ cho bạn thấy nó thực sự hoạt động như thế nào.

Thưởng thức, tôi biết tôi !!

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

với cách sử dụng tương tự như thế này:

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

Bây giờ mọi thứ vẫn tồn tại suôn sẻ ở mọi nơi trên tất cả các trang của tôi

Đẹp!

Cảm ơn Kent!


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

Bây giờ bạn sẽ thấy, đó là mã của bạn thực sự làm việc. Những gì không làm việc là console.log(movies). Điều này là do moviescác điểm đến trạng thái cũ . Nếu bạn di chuyển ra console.log(movies)bên ngoài useEffect, ngay phía trên trở lại, bạn sẽ thấy đối tượng phim được cập nhật.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.