Nhiều lệnh gọi đến trình cập nhật trạng thái từ useState trong thành phần gây ra nhiều kết xuất


114

Tôi đang thử React hooks lần đầu tiên và tất cả đều ổn cho đến khi tôi nhận ra rằng khi tôi lấy dữ liệu và cập nhật hai biến trạng thái khác nhau (dữ liệu và cờ tải), thành phần của tôi (một bảng dữ liệu) được hiển thị hai lần, mặc dù cả hai lần gọi đến trình cập nhật trạng thái đang diễn ra trong cùng một chức năng. Đây là hàm api của tôi đang trả về cả hai biến cho thành phần của tôi.

const getData = url => {

    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(async () => {

        const test = await api.get('/people')

        if(test.ok){
            setLoading(false);
            setData(test.data.results);
        }

    }, []);

    return { data, loading };
};

Trong một thành phần lớp bình thường, bạn sẽ thực hiện một lệnh gọi duy nhất để cập nhật trạng thái có thể là một đối tượng phức tạp nhưng "hooks way" dường như chia trạng thái thành các đơn vị nhỏ hơn, một tác dụng phụ của nó dường như là nhiều re- hiển thị khi chúng được cập nhật riêng. Bất kỳ ý tưởng làm thế nào để giảm thiểu điều này?


Nếu bạn đã phụ thuộc tiểu bang có lẽ bạn nên sử dụnguseReducer
thinklinux

Chà! Tôi chỉ mới phát hiện ra điều này và nó đã hoàn toàn thổi bay sự hiểu biết của tôi về cách hoạt động của kết xuất phản ứng. Tôi không thể hiểu bất kỳ lợi ích nào khi làm việc theo cách này - có vẻ khá tùy tiện khi hành vi trong lệnh gọi lại không đồng bộ khác với trong trình xử lý sự kiện bình thường. BTW, trong các thử nghiệm của tôi, có vẻ như việc điều chỉnh (tức là cập nhật DOM thực) không xảy ra cho đến khi tất cả các lệnh gọi setState đã được xử lý, vì vậy các lệnh gọi kết xuất trung gian vẫn bị lãng phí.
Andy

Câu trả lời:


113

Bạn có thể kết hợp loadingtrạng thái và datatrạng thái thành một đối tượng trạng thái và sau đó bạn có thể thực hiện một setStatelệnh gọi và sẽ chỉ có một kết xuất.

Lưu ý: Không giống như các setStatethành phần trong lớp, thành phần setStatetrả về từ useStatekhông hợp nhất các đối tượng với trạng thái hiện có, nó thay thế hoàn toàn đối tượng. Nếu bạn muốn hợp nhất, bạn sẽ cần phải đọc trạng thái trước đó và tự hợp nhất nó với các giá trị mới. Tham khảo tài liệu .

Tôi sẽ không lo lắng quá nhiều về việc gọi hiển thị quá mức cho đến khi bạn xác định được rằng mình gặp sự cố về hiệu suất. Kết xuất (trong bối cảnh React) và cam kết cập nhật DOM ảo với DOM thực là những vấn đề khác nhau. Kết xuất ở đây đề cập đến việc tạo DOM ảo chứ không phải cập nhật DOM của trình duyệt. React có thể thực hiện hàng loạt các setStatecuộc gọi và cập nhật DOM của trình duyệt với trạng thái mới cuối cùng.

const {useState, useEffect} = React;

function App() {
  const [userRequest, setUserRequest] = useState({
    loading: false,
    user: null,
  });

  useEffect(() => {
    // Note that this replaces the entire object and deletes user key!
    setUserRequest({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(results => results.json())
      .then(data => {
        setUserRequest({
          loading: false,
          user: data.results[0],
        });
      });
  }, []);

  const { loading, user } = userRequest;

  return (
    <div>
      {loading && 'Loading...'}
      {user && user.name.first}
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>

Thay thế - viết hook sáp nhập tiểu bang của riêng bạn

const {useState, useEffect} = React;

function useMergeState(initialState) {
  const [state, setState] = useState(initialState);
  const setMergedState = newState => 
    setState(prevState => Object.assign({}, prevState, newState)
  );
  return [state, setMergedState];
}

function App() {
  const [userRequest, setUserRequest] = useMergeState({
    loading: false,
    user: null,
  });

  useEffect(() => {
    setUserRequest({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(results => results.json())
      .then(data => {
        setUserRequest({
          loading: false,
          user: data.results[0],
        });
      });
  }, []);

  const { loading, user } = userRequest;

  return (
    <div>
      {loading && 'Loading...'}
      {user && user.name.first}
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('#app'));
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>


1
Tôi đồng ý rằng nó không lý tưởng, nhưng đó vẫn là một cách hợp lý để làm điều đó. Bạn có thể viết hook tùy chỉnh của riêng mình để hợp nhất nếu bạn không muốn tự mình hợp nhất. Tôi đã cập nhật câu cuối cùng để được rõ ràng hơn. Bạn có quen với cách hoạt động của React không? Nếu không, đây là một số liên kết có thể giúp bạn hiểu rõ hơn - reactjs.org/docs/reconccting.html , blog.vjeux.com/2013/javascript/react-performance.html .
Yangshun Tay

2
@jonhobbs Tôi đã thêm một câu trả lời thay thế để tạo một useMergeStatehook để bạn có thể hợp nhất trạng thái tự động.
Yangshun Tay

1
Tuyệt vời, cảm ơn một lần nữa. Tôi đã quen hơn với việc sử dụng React nhưng có thể học được nhiều điều về nội bộ của nó. Tôi sẽ đọc cho họ.
jonhobbs

2
Tò mò trong những trường hợp nào thì điều này có thể đúng ?: " React có thể thực hiện hàng loạt các setStatecuộc gọi và cập nhật DOM của trình duyệt với trạng thái mới cuối cùng. "
ecoe 25/09/19

1
Tuyệt vời .. tôi đã nghĩ đến việc kết hợp trạng thái hoặc sử dụng trạng thái tải trọng riêng biệt và để các trạng thái khác được đọc từ đối tượng tải trọng. Bài rất tốt mặc dù. 10/10 sẽ đọc lại
Anthony Moon Beam Toorie

59

Điều này cũng có một giải pháp sử dụng useReducer! đầu tiên chúng tôi xác định cái mới của chúng tôi setState.

const [state, setState] = useReducer(
  (state, newState) => ({...state, ...newState}),
  {loading: true, data: null, something: ''}
)

sau đó chúng ta có thể đơn giản sử dụng nó như các lớp cũ tốt this.setState, chỉ không có this!

setState({loading: false, data: test.data.results})

Như bạn có thể nhận thấy trong mới của chúng tôi setState(giống như những gì chúng tôi đã có trước đây this.setState), chúng tôi không cần cập nhật tất cả các trạng thái cùng nhau! ví dụ: tôi có thể thay đổi một trong các trạng thái của chúng ta như thế này (và nó không làm thay đổi các trạng thái khác!):

setState({loading: false})

Tuyệt vời, Ha ?!

Vì vậy, hãy đặt tất cả các phần lại với nhau:

import {useReducer} from 'react'

const getData = url => {
  const [state, setState] = useReducer(
    (state, newState) => ({...state, ...newState}),
    {loading: true, data: null}
  )

  useEffect(async () => {
    const test = await api.get('/people')
    if(test.ok){
      setState({loading: false, data: test.data.results})
    }
  }, [])

  return state
}

Expected 0 arguments, but got 1.ts(2554)là lỗi tôi nhận được khi cố sử dụng useReducer theo cách đó. Chính xác hơn setState. Làm thế nào để bạn khắc phục điều đó? Tệp là js, nhưng chúng tôi vẫn sử dụng kiểm tra ts.
ZenVentzi

Người anh em tuyệt vời, cảm ơn
Nisharg Shah

Tôi nghĩ rằng nó vẫn giống nhau khi hai tiểu bang hợp nhất. Nhưng trong một số trường hợp, bạn không thể hợp nhất các trạng thái.
user1888955

43

Cập nhật hàng loạt trong react-hooks https://github.com/facebook/react/issues/14259

React hiện sẽ cập nhật hàng loạt trạng thái nếu chúng được kích hoạt từ bên trong sự kiện dựa trên React, chẳng hạn như một lần nhấp vào nút hoặc thay đổi đầu vào. Nó sẽ không cập nhật hàng loạt nếu chúng được kích hoạt bên ngoài trình xử lý sự kiện React, chẳng hạn như lệnh gọi không đồng bộ.


5
Đây là một câu trả lời hữu ích, vì nó giải quyết được vấn đề trong hầu hết các trường hợp mà không cần phải làm gì cả. Cảm ơn!
davnicwil

7

Điều này sẽ làm:

const [state, setState] = useState({ username: '', password: ''});

// later
setState({
    ...state,
    username: 'John'
});

4

Nếu bạn đang sử dụng hook của bên thứ ba và không thể hợp nhất trạng thái thành một đối tượng hoặc sử dụng useReducer, thì giải pháp là sử dụng:

ReactDOM.unstable_batchedUpdates(() => { ... })

Được giới thiệu bởi Dan Abramov tại đây

Xem ví dụ này


Gần một năm sau, tính năng này dường như vẫn hoàn toàn không có tài liệu và vẫn được đánh dấu là không ổn định :-(
Andy

4

Để tái tạo this.setStatehành vi hợp nhất từ các thành phần giai cấp, Phản ứng tài liệu đề nghị sử dụng các hình thức chức năng của useStatevới đối tượng lây lan - Không cần cho useReducer:

setState(prevState => {
  return {...prevState, loading, data};
});

Hai trạng thái hiện được hợp nhất thành một, điều này sẽ giúp bạn tiết kiệm một chu kỳ kết xuất.

Có một lợi thế khác với một đối tượng trạng thái: loadingdatalà các trạng thái phụ thuộc . Các thay đổi trạng thái không hợp lệ trở nên rõ ràng hơn, khi trạng thái được kết hợp với nhau:

setState({ loading: true, data }); // ups... loading, but we already set data

Bạn thậm chí tốt hơn có thể đảm bảo các quốc gia phù hợp bằng 1.) làm cho tình trạng - loading, success, error, vv - rõ ràng trong tiểu bang và 2. bạn) sử dụng useReducerlogic trạng thái gói gọn trong một giảm:

const useData = () => {
  const [state, dispatch] = useReducer(reducer, /*...*/);

  useEffect(() => {
    api.get('/people').then(test => {
      if (test.ok) dispatch(["success", test.data.results]);
    });
  }, []);
};

const reducer = (state, [status, payload]) => {
  if (status === "success") return { ...state, data: payload, status };
  // keep state consistent, e.g. reset data, if loading
  else if (status === "loading") return { ...state, data: undefined, status };
  return state;
};

const App = () => {
  const { data, status } = useData();
  return status === "loading" ? <div> Loading... </div> : (
    // success, display data 
  )
}

Tái bút: Đảm bảo đặt tiền tố Hooks tùy chỉnh bằng use( useDatathay vì getData). Gọi lại cũng được chuyển đến useEffectkhông thể được async.


1
Đã ủng hộ cái này! Đây là một trong những tính năng hữu ích nhất mà tôi tìm thấy trong API React. Tìm thấy nó khi tôi cố gắng lưu trữ một hàm dưới dạng trạng thái (tôi biết thật kỳ lạ khi lưu trữ các hàm, nhưng vì lý do nào đó tôi không nhớ) và nó không hoạt động như mong đợi vì chữ ký cuộc gọi dành riêng này
elquimista

1

Một chút bổ sung cho câu trả lời https://stackoverflow.com/a/53575023/121143

Mát mẻ! Đối với những người đang có kế hoạch sử dụng hook này, nó có thể được viết theo cách mạnh mẽ một chút để làm việc với function as đối số, chẳng hạn như sau:

const useMergedState = initial => {
  const [state, setState] = React.useState(initial);
  const setMergedState = newState =>
    typeof newState == "function"
      ? setState(prevState => ({ ...prevState, ...newState(prevState) }))
      : setState(prevState => ({ ...prevState, ...newState }));
  return [state, setMergedState];
};

Cập nhật : phiên bản được tối ưu hóa, trạng thái sẽ không bị sửa đổi khi trạng thái một phần đến không bị thay đổi.

const shallowPartialCompare = (obj, partialObj) =>
  Object.keys(partialObj).every(
    key =>
      obj.hasOwnProperty(key) &&
      obj[key] === partialObj[key]
  );

const useMergedState = initial => {
  const [state, setState] = React.useState(initial);
  const setMergedState = newIncomingState =>
    setState(prevState => {
      const newState =
        typeof newIncomingState == "function"
          ? newIncomingState(prevState)
          : newIncomingState;
      return shallowPartialCompare(prevState, newState)
        ? prevState
        : { ...prevState, ...newState };
    });
  return [state, setMergedState];
};

Mát mẻ! Tôi sẽ chỉ kết thúc setMergedStatevới useMemo(() => setMergedState, []), bởi vì, như các tài liệu nói, setStatekhông thay đổi giữa các lần hiển thị React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.:, theo cách này, hàm setState không được tạo lại khi kết xuất.
tonix

0

Bạn cũng có thể sử dụng useEffectđể phát hiện sự thay đổi trạng thái và cập nhật các giá trị trạng thái khác cho phù hợp

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.