Cách hủy tìm nạp trên componentWillUnmount


90

Tôi nghĩ rằng tiêu đề nói lên tất cả. Cảnh báo màu vàng được hiển thị mỗi khi tôi ngắt kết nối một thành phần vẫn đang tìm nạp.

Bảng điều khiển

Cảnh báo: Không thể gọi setState(hoặc forceUpdate) trên một thành phần chưa được gắn kết. Đây là điều không cần chọn, nhưng ... Để khắc phục, hãy hủy tất cả các đăng ký và tác vụ không đồng bộ trong componentWillUnmountphương thức.

  constructor(props){
    super(props);
    this.state = {
      isLoading: true,
      dataSource: [{
        name: 'loading...',
        id: 'loading',
      }]
    }
  }

  componentDidMount(){
    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        this.setState({
          isLoading: false,
          dataSource: responseJson,
        }, function(){
        });
      })
      .catch((error) =>{
        console.error(error);
      });
  }

những gì là nó cảnh báo tôi không có vấn đề đó
Nima Moradi

câu hỏi được cập nhật
João Belo

bạn có hứa hẹn hay mã không đồng bộ để tìm nạp
nima moradi

thêm bạn lấy mã để qustion
Nima Moradi

Câu trả lời:


80

Khi bạn kích hoạt Lời hứa, có thể mất vài giây trước khi nó giải quyết và vào thời điểm đó, người dùng có thể đã điều hướng đến một vị trí khác trong ứng dụng của bạn. Vì vậy, khi giải quyết Promise setStateđược thực thi trên thành phần chưa được gắn kết và bạn gặp lỗi - giống như trong trường hợp của bạn. Điều này cũng có thể gây rò rỉ bộ nhớ.

Đó là lý do tại sao tốt nhất là di chuyển một số logic không đồng bộ của bạn ra khỏi các thành phần.

Nếu không, bằng cách nào đó, bạn sẽ cần phải hủy bỏ Lời hứa của mình . Ngoài ra - như một kỹ thuật cuối cùng (đó là một phản vật chất) - bạn có thể giữ một biến để kiểm tra xem thành phần có còn được gắn kết hay không:

componentDidMount(){
  this.mounted = true;

  this.props.fetchData().then((response) => {
    if(this.mounted) {
      this.setState({ data: response })
    }
  })
}

componentWillUnmount(){
  this.mounted = false;
}

Tôi sẽ nhấn mạnh điều đó một lần nữa - đây là một phản vật chất nhưng có thể đủ trong trường hợp của bạn (giống như họ đã làm với Formikviệc triển khai).

Một cuộc thảo luận tương tự trên GitHub

BIÊN TẬP:

Đây có lẽ là cách tôi giải quyết vấn đề tương tự (không có gì ngoài React) với Hooks :

LỰA CHỌN A:

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

export default function Page() {
  const value = usePromise("https://something.com/api/");
  return (
    <p>{value ? value : "fetching data..."}</p>
  );
}

function usePromise(url) {
  const [value, setState] = useState(null);

  useEffect(() => {
    let isMounted = true; // track whether component is mounted

    request.get(url)
      .then(result => {
        if (isMounted) {
          setState(result);
        }
      });

    return () => {
      // clean up
      isMounted = false;
    };
  }, []); // only on "didMount"

  return value;
}

LỰA CHỌN B: Ngoài ra useRef, nó hoạt động như một thuộc tính tĩnh của một lớp, nghĩa là nó không thực hiện kết xuất thành phần khi giá trị của nó thay đổi:

function usePromise2(url) {
  const isMounted = React.useRef(true)
  const [value, setState] = useState(null);


  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  useEffect(() => {
    request.get(url)
      .then(result => {
        if (isMounted.current) {
          setState(result);
        }
      });
  }, []);

  return value;
}

// or extract it to custom hook:
function useIsMounted() {
  const isMounted = React.useRef(true)

  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  return isMounted; // returning "isMounted.current" wouldn't work because we would return unmutable primitive
}

Ví dụ: https://codesandbox.io/s/86n1wq2z8


4
vì vậy không có cách thực sự nào để chỉ hủy tìm nạp trên componentWillUnmount?
João Belo

1
Ồ, tôi đã không nhận thấy mã câu trả lời của bạn trước đây, nó đã hoạt động. cảm ơn
João Belo


2
ý của bạn là gì khi "Đó là lý do tại sao tốt nhất là di chuyển logic không đồng bộ của bạn ra khỏi các thành phần."? Không phải mọi thứ trong react đều là một thành phần?
Karpik 14/09/18

1
@Tomasz Mularczyk Cảm ơn bạn rất nhiều, bạn đã làm những điều xứng đáng.
KARTHIKEYAN.A

25

Những người thân thiện tại React khuyên bạn nên gói các cuộc gọi / lời hứa tìm nạp của bạn trong một lời hứa có thể hủy bỏ. Mặc dù không có khuyến nghị nào trong tài liệu đó để giữ mã tách biệt với lớp hoặc hàm có tìm nạp, nhưng điều này có vẻ được khuyến khích vì các lớp và hàm khác có thể cần chức năng này, sao chép mã là một cách chống mẫu và bất kể mã còn tồn tại nên được xử lý hoặc hủy bỏ trong componentWillUnmount(). Theo React, bạn có thể gọi cancel()lời hứa được bao bọc trong componentWillUnmountđể tránh thiết lập trạng thái trên một thành phần chưa được gắn kết.

Đoạn mã được cung cấp sẽ trông giống như những đoạn mã này nếu chúng ta sử dụng React làm hướng dẫn:

const makeCancelable = (promise) => {
    let hasCanceled_ = false;

    const wrappedPromise = new Promise((resolve, reject) => {
        promise.then(
            val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
            error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
        );
    });

    return {
        promise: wrappedPromise,
        cancel() {
            hasCanceled_ = true;
        },
    };
};

const cancelablePromise = makeCancelable(fetch('LINK HERE'));

constructor(props){
    super(props);
    this.state = {
        isLoading: true,
        dataSource: [{
            name: 'loading...',
            id: 'loading',
        }]
    }
}

componentDidMount(){
    cancelablePromise.
        .then((response) => response.json())
        .then((responseJson) => {
            this.setState({
                isLoading: false,
                dataSource: responseJson,
            }, () => {

            });
        })
        .catch((error) =>{
            console.error(error);
        });
}

componentWillUnmount() {
    cancelablePromise.cancel();
}

---- BIÊN TẬP ----

Tôi nhận thấy câu trả lời đã cho có thể không hoàn toàn chính xác khi theo dõi vấn đề trên GitHub. Đây là một phiên bản mà tôi sử dụng phù hợp với mục đích của tôi:

export const makeCancelableFunction = (fn) => {
    let hasCanceled = false;

    return {
        promise: (val) => new Promise((resolve, reject) => {
            if (hasCanceled) {
                fn = null;
            } else {
                fn(val);
                resolve(val);
            }
        }),
        cancel() {
            hasCanceled = true;
        }
    };
};

Ý tưởng là giúp bộ thu gom rác giải phóng bộ nhớ bằng cách tạo hàm hoặc bất cứ điều gì bạn sử dụng là null.


bạn có liên kết đến vấn đề trên github không
Ren

@Ren, có một trang GitHub để chỉnh sửa trang và thảo luận các vấn đề.
haleonj

Tôi không còn chắc chắn vấn đề chính xác nằm ở đâu trong dự án GitHub đó.
haleonj

1
Liên kết đến sự cố GitHub: github.com/facebook/react/issues/5465
sammalfix

22

Bạn có thể dùng AbortController để hủy yêu cầu tìm nạp.

Xem thêm: https://www.npmjs.com/package/abortcontroller-polyfill

class FetchComponent extends React.Component{
  state = { todos: [] };
  
  controller = new AbortController();
  
  componentDidMount(){
    fetch('https://jsonplaceholder.typicode.com/todos',{
      signal: this.controller.signal
    })
    .then(res => res.json())
    .then(todos => this.setState({ todos }))
    .catch(e => alert(e.message));
  }
  
  componentWillUnmount(){
    this.controller.abort();
  }
  
  render(){
    return null;
  }
}

class App extends React.Component{
  state = { fetch: true };
  
  componentDidMount(){
    this.setState({ fetch: false });
  }
  
  render(){
    return this.state.fetch && <FetchComponent/>
  }
}

ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>


2
Tôi ước mình đã biết rằng có một API Web để hủy các yêu cầu như AbortController. Nhưng không sao, biết cũng chưa muộn. Cảm ơn bạn.
Lex Soft

11

Vì bài đăng đã được mở, một "tìm nạp có thể hủy bỏ" đã được thêm vào. https://developers.google.com/web/updates/2017/09/abortable-fetch

(từ tài liệu :)

Bộ điều khiển + cơ động tín hiệu Gặp gỡ AbortController và AbortSignal:

const controller = new AbortController();
const signal = controller.signal;

Bộ điều khiển chỉ có một phương pháp:

controller.abort (); Khi bạn làm điều này, nó sẽ thông báo tín hiệu:

signal.addEventListener('abort', () => {
  // Logs true:
  console.log(signal.aborted);
});

API này được cung cấp bởi tiêu chuẩn DOM và đó là toàn bộ API. Nó cố tình chung chung để nó có thể được sử dụng bởi các tiêu chuẩn web và thư viện JavaScript khác.

ví dụ: đây là cách bạn đặt thời gian chờ tìm nạp sau 5 giây:

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal }).then(response => {
  return response.text();
}).then(text => {
  console.log(text);
});

Thật thú vị, tôi sẽ thử cách này. Nhưng trước đó, tôi sẽ đọc API AbortController trước.
Lex Soft

Chúng ta có thể sử dụng chỉ một phiên bản AbortController cho nhiều lần tìm nạp để khi chúng ta gọi phương thức hủy bỏ của AbortController duy nhất này trong componentWillUnmount, nó sẽ hủy tất cả các lần tìm nạp hiện có trong thành phần của chúng ta không? Nếu không, có nghĩa là chúng ta phải cung cấp các phiên bản AbortController khác nhau cho mỗi lần tìm nạp, phải không?
Lex Soft

3

Điểm mấu chốt của cảnh báo này là thành phần của bạn có tham chiếu đến nó được nắm giữ bởi một số lời hứa / gọi lại nổi bật.

Để tránh phản vật chất của việc giữ trạng thái isMounted của bạn xung quanh (giữ cho thành phần của bạn tồn tại) như đã được thực hiện trong mẫu thứ hai, trang web phản ứng đề xuất sử dụng một lời hứa tùy chọn ; tuy nhiên mã đó cũng xuất hiện để giữ cho đối tượng của bạn tồn tại.

Thay vào đó, tôi đã làm điều đó bằng cách sử dụng một bao đóng với một hàm ràng buộc lồng nhau để setState.

Đây là phương thức khởi tạo của tôi (typecript)…

constructor(props: any, context?: any) {
    super(props, context);

    let cancellable = {
        // it's important that this is one level down, so we can drop the
        // reference to the entire object by setting it to undefined.
        setState: this.setState.bind(this)
    };

    this.componentDidMount = async () => {
        let result = await fetch(…);            
        // ideally we'd like optional chaining
        // cancellable.setState?.({ url: result || '' });
        cancellable.setState && cancellable.setState({ url: result || '' });
    }

    this.componentWillUnmount = () => {
        cancellable.setState = undefined; // drop all references.
    }
}

3
Đây là khái niệm không có gì khác hơn là giữ một lá cờ isMounted, chỉ có bạn đang ràng buộc nó vào đóng cửa thay vì treo nó trongthis
AnilRedshift

2

Khi tôi cần "hủy tất cả đăng ký và không đồng bộ", tôi thường gửi một cái gì đó đến redux trong componentWillUnmount để thông báo cho tất cả những người đăng ký khác và gửi thêm một yêu cầu về việc hủy đến máy chủ nếu cần


2

Tôi nghĩ nếu không cần thiết phải thông báo cho máy chủ về việc hủy bỏ - thì cách tốt nhất là chỉ sử dụng cú pháp async / await (nếu có).

constructor(props){
  super(props);
  this.state = {
    isLoading: true,
    dataSource: [{
      name: 'loading...',
      id: 'loading',
    }]
  }
}

async componentDidMount() {
  try {
    const responseJson = await fetch('LINK HERE')
      .then((response) => response.json());

    this.setState({
      isLoading: false,
      dataSource: responseJson,
    }
  } catch {
    console.error(error);
  }
}

0

Ngoài các ví dụ về móc câu hứa có thể hủy trong giải pháp được chấp nhận, có thể hữu ích khi có một useAsyncCallbackmóc bao bọc một yêu cầu gọi lại và trả lại một lời hứa có thể hủy. Ý tưởng là giống nhau, nhưng với một cái móc hoạt động giống như một cái bình thường useCallback. Đây là một ví dụ về cách triển khai:

function useAsyncCallback<T, U extends (...args: any[]) => Promise<T>>(callback: U, dependencies: any[]) {
  const isMounted = useRef(true)

  useEffect(() => {
    return () => {
      isMounted.current = false
    }
  }, [])

  const cb = useCallback(callback, dependencies)

  const cancellableCallback = useCallback(
    (...args: any[]) =>
      new Promise<T>((resolve, reject) => {
        cb(...args).then(
          value => (isMounted.current ? resolve(value) : reject({ isCanceled: true })),
          error => (isMounted.current ? reject(error) : reject({ isCanceled: true }))
        )
      }),
    [cb]
  )

  return cancellableCallback
}

-2

Tôi nghĩ rằng tôi đã tìm ra một cách xung quanh nó. Vấn đề không nằm ở việc tìm nạp chính nó mà là setState sau khi thành phần bị loại bỏ. Vì vậy, giải pháp là đặt this.state.isMountedthành falsevà sau đó componentWillMountthay đổi nó thành true, và componentWillUnmountđặt lại thành false. Sau đó, chỉ if(this.state.isMounted)setState bên trong tìm nạp. Như vậy:

  constructor(props){
    super(props);
    this.state = {
      isMounted: false,
      isLoading: true,
      dataSource: [{
        name: 'loading...',
        id: 'loading',
      }]
    }
  }

  componentDidMount(){
    this.setState({
      isMounted: true,
    })

    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        if(this.state.isMounted){
          this.setState({
            isLoading: false,
            dataSource: responseJson,
          }, function(){
          });
        }
      })
      .catch((error) =>{
        console.error(error);
      });
  }

  componentWillUnmount() {
    this.setState({
      isMounted: false,
    })
  }

3
setState có lẽ không phải là lý tưởng, vì nó sẽ không cập nhật giá trị ở trạng thái ngay lập tức.
LeonF
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.