React Hook Cảnh báo về hàm không đồng bộ đang được sử dụngEffect: useEffect function phải trả về một hàm dọn dẹp hoặc không có gì


115

Tôi đang thử ví dụ useEffect như sau:

useEffect(async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}, []);

và tôi nhận được cảnh báo này trong bảng điều khiển của mình. Nhưng tôi nghĩ rằng việc dọn dẹp là tùy chọn đối với các cuộc gọi không đồng bộ. Tôi không chắc tại sao tôi nhận được cảnh báo này. Liên kết hộp cát để làm ví dụ. https://codesandbox.io/s/24rj871r0p nhập mô tả hình ảnh ở đây

Câu trả lời:


161

Tôi khuyên bạn nên xem câu trả lời của Dan Abramov (một trong những người duy trì cốt lõi React) ở đây :

Tôi nghĩ bạn đang làm cho nó phức tạp hơn mức cần thiết.

function Example() {
  const [data, dataSet] = useState<any>(null)

  useEffect(() => {
    async function fetchMyAPI() {
      let response = await fetch('api/data')
      response = await response.json()
      dataSet(response)
    }

    fetchMyAPI()
  }, [])

  return <div>{JSON.stringify(data)}</div>
}

Về lâu dài, chúng tôi sẽ không khuyến khích mô hình này vì nó khuyến khích các điều kiện chủng tộc. Chẳng hạn như - bất cứ điều gì có thể xảy ra giữa cuộc gọi của bạn bắt đầu và kết thúc, và bạn có thể nhận được các đạo cụ mới. Thay vào đó, chúng tôi sẽ khuyên bạn nên Tạm ngưng để tìm nạp dữ liệu trông giống

const response = MyAPIResource.read();

và không có hiệu ứng. Nhưng trong thời gian chờ đợi, bạn có thể di chuyển nội dung không đồng bộ sang một chức năng riêng biệt và gọi nó.

Bạn có thể đọc thêm về hồi hộp thử nghiệm tại đây .


Nếu bạn muốn sử dụng các chức năng bên ngoài với eslint.

 function OutsideUsageExample() {
  const [data, dataSet] = useState<any>(null)

  const fetchMyAPI = useCallback(async () => {
    let response = await fetch('api/data')
    response = await response.json()
    dataSet(response)
  }, [])

  useEffect(() => {
    fetchMyAPI()
  }, [fetchMyAPI])

  return (
    <div>
      <div>data: {JSON.stringify(data)}</div>
      <div>
        <button onClick={fetchMyAPI}>manual fetch</button>
      </div>
    </div>
  )
}

Với useCallback useCallback . Hộp cát .

import React, { useState, useEffect, useCallback } from "react";

export default function App() {
  const [counter, setCounter] = useState(1);

  // if counter is changed, than fn will be updated with new counter value
  const fn = useCallback(() => {
    setCounter(counter + 1);
  }, [counter]);

  // if counter is changed, than fn will not be updated and counter will be always 1 inside fn
  /*const fnBad = useCallback(() => {
      setCounter(counter + 1);
    }, []);*/

  // if fn or counter is changed, than useEffect will rerun
  useEffect(() => {
    if (!(counter % 2)) return; // this will stop the loop if counter is not even

    fn();
  }, [fn, counter]);

  // this will be infinite loop because fn is always changing with new counter value
  /*useEffect(() => {
    fn();
  }, [fn]);*/

  return (
    <div>
      <div>Counter is {counter}</div>
      <button onClick={fn}>add +1 count</button>
    </div>
  );
}

Bạn có thể giải quyết các vấn đề về điều kiện cuộc đua bằng cách kiểm tra xem thành phần có được tháo ra như vậy không: useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])
Richard

1
Bạn cũng có thể sử dụng một gói có tên use-async-effect . Gói này cho phép bạn sử dụng cú pháp async await.
KittyCat

Sử dụng một hàm tự gọi không để async bị rò rỉ đến định nghĩa hàm useEffect hoặc triển khai tùy chỉnh một hàm kích hoạt lệnh gọi async như một trình bao bọc xung quanh useEffect là cách tốt nhất lúc này. Mặc dù bạn có thể bao gồm một gói mới như use-async-effect được đề xuất, tôi nghĩ đây là một vấn đề đơn giản cần giải quyết.
Thulani Chivandikwa

1
Này, điều đó ổn và tôi thường làm gì. nhưng eslintyêu cầu tôi làm cho fetchMyAPI()phụ thuộc củauseEffect
Prakash Reddy Potlapadu

51

Khi bạn sử dụng một hàm không đồng bộ như

async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}

nó trả lại một lời hứa và useEffect không mong đợi hàm gọi lại trả về Promise, đúng hơn nó mong đợi rằng không có gì được trả về hoặc một hàm được trả về.

Để giải quyết cảnh báo, bạn có thể sử dụng chức năng không đồng bộ tự gọi.

useEffect(() => {
    (async function() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    })();
}, []);

hoặc để làm cho nó gọn gàng hơn, bạn có thể xác định một hàm và sau đó gọi nó

useEffect(() => {
    async function fetchData() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    };
    fetchData();
}, []);

giải pháp thứ hai sẽ giúp bạn dễ đọc hơn và sẽ giúp bạn viết mã để hủy các yêu cầu trước đó nếu một yêu cầu mới được kích hoạt hoặc lưu phản hồi yêu cầu mới nhất ở trạng thái

Hộp mã hoạt động


Một gói để làm cho điều này dễ dàng hơn đã được thực hiện. Bạn có thể tìm thấy nó ở đây .
KittyCat

1
nhưng eslint sẽ không tha thứ với điều đó
Muhaimin CS

1
không có cách nào để thực hiện dọn dẹp / gọi lại didmount
David Rearte

1
@ShubhamKhatri khi bạn sử dụng, useEffectbạn có thể trả về một hàm để thực hiện việc dọn dẹp như hủy đăng ký các sự kiện. Khi bạn sử dụng hàm async, bạn không thể trả về bất cứ thứ gì vì useEffectsẽ không đợi kết quả
David Rearte

2
bạn nói rằng tôi có thể đặt một chức năng dọn dẹp trong một chức năng không đồng bộ? tôi đã thử nhưng chức năng dọn dẹp của tôi không bao giờ được gọi. Bạn có thể làm một ví dụ nhỏ?
David Rearte

32

Cho đến Phản ứng cung cấp một cách tốt hơn, bạn có thể tạo ra một helper, useEffectAsync.js:

import { useEffect } from 'react';


export default function useEffectAsync(effect, inputs) {
    useEffect(() => {
        effect();
    }, inputs);
}

Bây giờ bạn có thể chuyển một hàm không đồng bộ:

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);

8
Lý do React không tự động cho phép các hàm không đồng bộ được sử dụngEffect là trong một phần lớn các trường hợp, cần phải dọn dẹp một số. Hàm useAsyncEffectnhư bạn đã viết có thể dễ dàng đánh lừa ai đó suy nghĩ nếu họ trả về một hàm dọn dẹp từ hiệu ứng không đồng bộ của chúng, nó sẽ được chạy vào thời điểm thích hợp. Điều này có thể dẫn đến rò rỉ bộ nhớ hoặc các lỗi tồi tệ hơn, vì vậy chúng tôi đã chọn khuyến khích mọi người cấu trúc lại mã của họ để làm cho “đường nối” của các hàm không đồng bộ tương tác với vòng đời của React hiển thị rõ ràng hơn và hành vi của mã do đó hy vọng sẽ cân nhắc và đúng đắn hơn.
Sophie Alpert

8

Tôi đã đọc qua câu hỏi này và cảm thấy cách tốt nhất để triển khai useEffect không được đề cập trong câu trả lời. Giả sử bạn có một cuộc gọi mạng và muốn thực hiện điều gì đó khi bạn có phản hồi. Để đơn giản, hãy lưu trữ phản hồi mạng trong một biến trạng thái. Người ta có thể muốn sử dụng hành động / trình giảm thiểu để cập nhật cửa hàng với phản hồi mạng.

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

/* This would be called on initial page load */
useEffect(()=>{
    fetch(`https://www.reddit.com/r/${subreddit}.json`)
    .then(data => {
        setData(data);
    })
    .catch(err => {
        /* perform error handling if desired */
    });
}, [])

/* This would be called when store/state data is updated */
useEffect(()=>{
    if (data) {
        setPosts(data.children.map(it => {
            /* do what you want */
        }));
    }
}, [data]);

Tham khảo => https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects


1

thử

const MyFunctionnalComponent: React.FC = props => {
  useEffect(() => {
    // Using an IIFE
    (async function anyNameFunction() {
      await loadContent();
    })();
  }, []);
  return <div></div>;
};


1

Đối với những người đọc khác, lỗi có thể do không có dấu ngoặc bao bọc hàm không đồng bộ:

Xem xét hàm không đồng bộ initData

  async function initData() {
  }

Mã này sẽ dẫn đến lỗi của bạn:

  useEffect(() => initData(), []);

Nhưng cái này, sẽ không:

  useEffect(() => { initData(); }, []);

(Chú ý dấu ngoặc quanh initData ()


1
Tuyệt vời, anh bạn! Tôi đang sử dụng saga và lỗi đó đã xuất hiện khi tôi gọi một trình tạo hành động trả về đối tượng duy nhất. Có vẻ như hàm gọi lại useEffect không lấp liếm hành vi này. Tôi đánh giá cao câu trả lời của bạn.
Gorr1995

1
Đề phòng mọi người tự hỏi tại sao điều này lại đúng ... Không có dấu ngoặc nhọn, giá trị trả về của initData () được hàm mũi tên trả về một cách ngầm định. Với dấu ngoặc nhọn, không có gì được trả về một cách ngầm định và do đó lỗi sẽ không xảy ra.
Marnix.hoh

1

toán tử void có thể được sử dụng ở đây.
Thay vì:

React.useEffect(() => {
    async function fetchData() {
    }
    fetchData();
}, []);

hoặc là

React.useEffect(() => {
    (async function fetchData() {
    })()
}, []);

bạn có thể viết:

React.useEffect(() => {
    void async function fetchData() {
    }();
}, []);

Nó sạch hơn và đẹp hơn một chút.


Hiệu ứng không đồng bộ có thể gây rò rỉ bộ nhớ, vì vậy điều quan trọng là phải thực hiện dọn dẹp khi ngắt kết nối thành phần. Trong trường hợp tìm nạp, nó có thể trông như thế này:

function App() {
    const [ data, setData ] = React.useState([]);

    React.useEffect(() => {
        const abortController = new AbortController();
        void async function fetchData() {
            try {
                const url = 'https://jsonplaceholder.typicode.com/todos/1';
                const response = await fetch(url, { signal: abortController.signal });
                setData(await response.json());
            } catch (error) {
                console.log('error', error);
            }
        }();
        return () => {
            abortController.abort(); // cancel pending fetch request on component unmount
        };
    }, []);

    return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
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.