วิธียกเลิกการดึงข้อมูลบน componentWillUnmount


92

ฉันคิดว่าชื่อมันบอกทุกอย่าง คำเตือนสีเหลืองจะปรากฏขึ้นทุกครั้งที่ฉันยกเลิกการต่อเชื่อมส่วนประกอบที่ยังคงดึงข้อมูล

คอนโซล

คำเตือน: ไม่สามารถเรียกsetState(หรือforceUpdate) บนส่วนประกอบที่ไม่ได้ต่อเชื่อม นี่ไม่ใช่การดำเนินการ แต่ ... ในการแก้ไขให้ยกเลิกการสมัครสมาชิกและงานอะซิงโครนัสทั้งหมดในcomponentWillUnmountเมธอด

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

มันเตือนอะไรฉันไม่มีปัญหา
nima moradi

อัปเดตคำถาม
João Belo

คุณสัญญาหรือรหัส async สำหรับการดึงข้อมูล
nima moradi

เพิ่มคุณดึงรหัสไปยัง qustion
nima moradi

ดูisMounted เป็น Antipatternและยกเลิกการเรียกข้อมูล
mb21

คำตอบ:


82

เมื่อคุณส่งคำสัญญาอาจใช้เวลาสองสามวินาทีก่อนที่จะแก้ไขและเมื่อถึงเวลานั้นผู้ใช้อาจนำทางไปยังที่อื่นในแอปของคุณ ดังนั้นเมื่อ Promise แก้ไขsetStateถูกดำเนินการกับส่วนประกอบที่ไม่ได้ต่อเชื่อมและคุณได้รับข้อผิดพลาดเช่นเดียวกับในกรณีของคุณ นอกจากนี้ยังอาจทำให้หน่วยความจำรั่วไหล

นั่นเป็นเหตุผลที่ดีที่สุดที่จะย้ายตรรกะอะซิงโครนัสบางส่วนของคุณออกจากส่วนประกอบ

มิฉะนั้นคุณจะต้องอย่างใดยกเลิกสัญญาของคุณ อีกทางเลือกหนึ่ง - เป็นเทคนิคสุดท้าย (เป็นแอนตี้แพตเทิร์น) - คุณสามารถเก็บตัวแปรไว้เพื่อตรวจสอบว่าส่วนประกอบยังคงติดตั้งอยู่:

componentDidMount(){
  this.mounted = true;

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

componentWillUnmount(){
  this.mounted = false;
}

ฉันจะเน้นย้ำอีกครั้ง - นี่เป็นรูปแบบการต่อต้านแต่อาจเพียงพอในกรณีของคุณ (เช่นเดียวกับที่ทำกับFormikการนำไปใช้งาน)

การสนทนาที่คล้ายกันในGitHub

แก้ไข:

นี่อาจเป็นวิธีที่ฉันจะแก้ปัญหาเดียวกัน (ไม่มีอะไรเลยนอกจากตอบสนอง) ด้วยHooks :

ตัวเลือก 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;
}

ตัวเลือก B:หรืออีกวิธีหนึ่งที่มีuseRefพฤติกรรมเหมือนคุณสมบัติคงที่ของคลาสซึ่งหมายความว่าจะไม่ทำให้องค์ประกอบแสดงผลเมื่อค่าเปลี่ยนแปลง:

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
}

ตัวอย่าง: https://codesandbox.io/s/86n1wq2z8


4
ดังนั้นจึงไม่มีวิธีที่แท้จริงในการยกเลิกการดึงข้อมูลบน Component
João Belo

1
โอ้ฉันไม่ได้สังเกตรหัสของคำตอบของคุณมาก่อนมันใช้งานได้ ขอบคุณ
João Belo

10
ดูisMounted เป็น Antipatternและยกเลิกการเรียกข้อมูล
mb21

2
คุณหมายถึงอะไรโดย "นั่นเป็นเหตุผลที่ดีที่สุดที่จะย้ายตรรกะอะซิงโครนัสของคุณออกจากส่วนประกอบ"? ทุกสิ่งในปฏิกิริยาไม่เป็นส่วนประกอบ?
Karpik

1
@Tomasz Mularczyk ขอบคุณมากคุณทำสิ่งที่มีค่า
KARTHIKEYAN.A

27

ผู้คนที่เป็นมิตรที่ React ขอแนะนำให้ตัดสายเรียกเข้า / สัญญาของคุณด้วยสัญญาที่ยกเลิก แม้ว่าจะไม่มีคำแนะนำในเอกสารประกอบนั้นให้แยกรหัสออกจากคลาสหรือฟังก์ชันด้วยการดึงข้อมูล แต่ดูเหมือนว่าแนะนำให้ใช้เนื่องจากคลาสและฟังก์ชันอื่น ๆ มีแนวโน้มที่จะต้องการฟังก์ชันนี้การทำซ้ำรหัสเป็นรูปแบบที่ต่อต้านและไม่คำนึงถึงรหัสที่ค้างอยู่ componentWillUnmount()ควรทิ้งหรือยกเลิกไปใน ตามการตอบสนองคุณสามารถเรียกcancel()ใช้สัญญาที่ห่อไว้componentWillUnmountเพื่อหลีกเลี่ยงการตั้งค่าสถานะในส่วนประกอบที่ไม่ได้ต่อเชื่อม

โค้ดที่ให้มาจะมีลักษณะคล้ายกับข้อมูลโค้ดเหล่านี้หากเราใช้ React เป็นแนวทาง:

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

---- แก้ไข ----

ฉันพบว่าคำตอบที่ให้มาอาจไม่ถูกต้องนักโดยทำตามปัญหาใน GitHub นี่คือเวอร์ชันหนึ่งที่ฉันใช้ซึ่งเหมาะกับวัตถุประสงค์ของฉัน:

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

แนวคิดนี้คือการช่วยตัวเก็บขยะเพิ่มหน่วยความจำโดยการสร้างฟังก์ชันหรืออะไรก็ตามที่คุณใช้ค่าว่าง


คุณมีลิงก์ไปยังปัญหาใน github หรือไม่
Ren

@Ren มีไซต์ GitHub สำหรับแก้ไขหน้าและพูดคุยเกี่ยวกับปัญหา
haleonj

ฉันไม่แน่ใจอีกต่อไปว่าปัญหาที่แท้จริงของโครงการ GitHub นั้นอยู่ที่ไหน
haleonj

1
ลิงก์ไปยังปัญหา GitHub: github.com/facebook/react/issues/5465
sammalfix

23

คุณสามารถใช้AbortControllerเพื่อยกเลิกคำขอดึงข้อมูล

ดูเพิ่มเติม: 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
ฉันหวังว่าฉันจะรู้ว่ามี Web API สำหรับการยกเลิกคำขอเช่น AbortController แต่เอาล่ะยังไม่สายเกินไปที่จะรู้ ขอขอบคุณ.
Lex Soft

ดังนั้นถ้าคุณมีหลายfetches คุณสามารถส่งผ่าน single นั้นAbortControllerไปให้ทั้งหมดได้หรือไม่?
serg06

11

เนื่องจากมีการเปิดโพสต์จึงมีการเพิ่ม "ยกเลิกการดึงข้อมูล" https://developers.google.com/web/updates/2017/09/abortable-fetch

(จากเอกสาร :)

ตัวควบคุม + การซ้อมรบสัญญาณพบกับ AbortController และ AbortSignal:

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

คอนโทรลเลอร์มีวิธีเดียวเท่านั้น:

controller.abort (); เมื่อคุณทำสิ่งนี้จะแจ้งสัญญาณ:

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

API นี้จัดทำโดยมาตรฐาน DOM และนั่นคือ API ทั้งหมด เป็นแบบทั่วไปเพื่อให้สามารถใช้กับมาตรฐานเว็บอื่น ๆ และไลบรารี JavaScript ได้

ตัวอย่างเช่นต่อไปนี้เป็นวิธีที่คุณกำหนดระยะหมดเวลาการดึงข้อมูลหลังจากผ่านไป 5 วินาที:

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

น่าสนใจฉันจะลองวิธีนี้ แต่ก่อนหน้านั้นฉันจะอ่าน AbortController API ก่อน
Lex Soft

เราสามารถใช้ AbortController เพียงอินสแตนซ์เดียวสำหรับการดึงข้อมูลหลายรายการเช่นเมื่อเราเรียกใช้วิธีการยกเลิกของ AbortController เดียวนี้ใน componentWillUnmount มันจะยกเลิกการดึงข้อมูลที่มีอยู่ทั้งหมดในส่วนประกอบของเราหรือไม่ ถ้าไม่แสดงว่าเราต้องจัดเตรียมอินสแตนซ์ AbortController ที่แตกต่างกันสำหรับการดึงข้อมูลแต่ละรายการใช่ไหม
Lex Soft

3

จุดสำคัญของคำเตือนนี้คือส่วนประกอบของคุณมีการอ้างอิงถึงคำเตือนที่มีการเรียกกลับ / คำสัญญาที่โดดเด่น

เพื่อหลีกเลี่ยงการ antipattern ของการรักษารัฐ isMounted รอบของคุณ (ซึ่งทำให้องค์ประกอบชีวิตของคุณ) ดังที่ทำในรูปแบบที่สองตอบสนองเว็บไซต์แสดงให้เห็นการใช้สัญญาไม่จำเป็น ; อย่างไรก็ตามรหัสนั้นดูเหมือนจะทำให้วัตถุของคุณมีชีวิตอยู่

แต่ฉันได้ทำโดยใช้การปิดด้วยฟังก์ชันผูกซ้อนกับ setState

นี่คือตัวสร้างของฉัน (typescript) ...

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
แนวคิดนี้ไม่แตกต่างไปจากการเก็บธง isMounted เพียงคุณผูกมันไว้กับการปิดแทนที่จะแขวนไว้this
AnilRedshift

2

เมื่อฉันต้องการ "ยกเลิกการสมัครสมาชิกทั้งหมดและแบบอะซิงโครนัส" ฉันมักจะส่งบางสิ่งไปยัง redux ใน componentWillUnmount เพื่อแจ้งสมาชิกรายอื่นทั้งหมดและส่งคำขออีกครั้งหนึ่งเกี่ยวกับการยกเลิกไปยังเซิร์ฟเวอร์หากจำเป็น


2

ฉันคิดว่าหากไม่จำเป็นต้องแจ้งเซิร์ฟเวอร์เกี่ยวกับการยกเลิก - แนวทางที่ดีที่สุดคือใช้ async / await syntax (ถ้ามี)

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

นอกเหนือจากตัวอย่าง hooks สัญญาที่ยกเลิกได้ในโซลูชันที่ยอมรับแล้วยังมีประโยชน์ในการมีuseAsyncCallbackhook สำหรับการเรียกกลับคำขอและส่งคืนสัญญาที่ยกเลิกได้ ความคิดที่เหมือนกัน useCallbackแต่มีตะขอทำงานเหมือนปกติ นี่คือตัวอย่างการใช้งาน:

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
}

0

เมื่อใช้แพ็คเกจCPromiseคุณสามารถยกเลิกโซ่สัญญาของคุณรวมถึงกลุ่มที่ซ้อนกันได้ สนับสนุน AbortController และเครื่องกำเนิดไฟฟ้าเพื่อทดแทนฟังก์ชัน ECMA async ด้วยการใช้มัณฑนากร CPromise คุณสามารถจัดการงาน async ของคุณได้อย่างง่ายดายทำให้สามารถยกเลิกได้

นักตกแต่งใช้การสาธิตสด :

import { async, listen, cancel, timeout } from "c-promise2";
import cpFetch from "cp-fetch";

export class TestComponent extends React.Component {
  state = {
    text: ""
  };

  @timeout(5000)
  @listen
  @async
  *componentDidMount() {
    console.log("mounted");
    const response = yield cpFetch(this.props.url);
    this.setState({ text: `json: ${yield response.text()}` });
  }

  render() {
    return <div>{this.state.text}</div>;
  }

  @cancel()
  componentWillUnmount() {
    console.log("unmounted");
  }
}

ทุกขั้นตอนสามารถยกเลิก / ยกเลิกได้อย่างสมบูรณ์ นี่คือตัวอย่างการใช้กับ React Live Demo

export class TestComponent extends React.Component {
  state = {};

  async componentDidMount() {
    console.log("mounted");
    this.controller = new CPromise.AbortController();
    try {
      const json = await this.myAsyncTask(
        "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s"
      );
      console.log("json:", json);
      await this.myAsyncTaskWithDelay(1000, 123); // just another async task
      this.setState({ text: JSON.stringify(json) });
    } catch (err) {
      if (CPromise.isCanceledError(err)) {
        console.log("tasks terminated");
      }
    }
  }

  myAsyncTask(url) {
    return CPromise.from(function* () {
      const response = yield cpFetch(url); // cancellable request
      return yield response.json();
    }).listen(this.controller.signal);
  }

  myAsyncTaskWithDelay(ms, value) {
    return new CPromise((resolve, reject, { onCancel }) => {
      const timer = setTimeout(resolve, ms, value);
      onCancel(() => {
        console.log("timeout cleared");
        clearTimeout(timer);
      });
    }).listen(this.controller.signal);
  }

  render() {
    return (
      <div>
        AsyncComponent: <span>{this.state.text || "fetching..."}</span>
      </div>
    );
  }
  componentWillUnmount() {
    console.log("unmounted");
    this.controller.abort(); // kill all pending tasks
  }
}

ใช้ตะขอและcancelวิธีการ

import React, { useEffect, useState } from "react";
import CPromise from "c-promise2";
import cpFetch from "cp-fetch";

export function TestComponent(props) {
  const [text, setText] = useState("fetching...");

  useEffect(() => {
    console.log("mount");
    // all stages here are completely cancellable
    const promise = cpFetch(props.url)
      .then(function* (response) {
        const json = yield response.json();
        setText(`Delay for 2000ms...`);
        yield CPromise.delay(2000);
        setText(`Success: ${JSON.stringify(json)}`);
      })
      .canceled()
      .catch((err) => {
        setText(`Failed: ${err}`);
      });

    return () => {
      console.log("unmount");
      promise.cancel();
    };
  }, [props.url]);

  return <p>{text}</p>;
}

-2

ฉันคิดว่าฉันหาทางแก้ไขได้แล้ว ปัญหาไม่มากเท่ากับการดึงข้อมูลเอง แต่ setState หลังจากที่คอมโพเนนต์ถูกปิด ดังนั้นวิธีแก้ปัญหาคือตั้งค่าthis.state.isMountedเป็นfalseแล้วcomponentWillMountเปลี่ยนเป็นจริงและcomponentWillUnmountตั้งค่าเป็นเท็จอีกครั้ง จากนั้นเพียงแค่if(this.state.isMounted)setState ภายในการดึงข้อมูล ชอบมาก:

  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 อาจไม่เหมาะเนื่องจากจะไม่อัปเดตค่าในสถานะทันที
LeonF
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.