javascript - ไม่สามารถทำการอัปเดตสถานะของ React บนคอมโพเนนต์ที่ถูกยกเลิกการติดตั้งได้
ปัญหา
ฉันกำลังเขียนแอปพลิเคชันใน React และไม่สามารถหลีกเลี่ยงข้อผิดพลาดทั่วไปมาก ๆ ซึ่งก็คือการเรียก setState(...) หลังจาก componentWillUnmount(...).
ฉันได้ตรวจสอบโค้ดของฉันอย่างระมัดระวังและพยายามใส่เงื่อนไขป้องกันบางอย่าง แต่ปัญหายังคงอยู่และฉันยังคงเห็นคำเตือนอยู่
ดังนั้น ฉันมีคำถามสองข้อ:
- ฉันจะหาวิธีจาก stack trace ว่าส่วนประกอบและผู้จัดการเหตุการณ์หรือ hook ของ lifecycle ใดที่รับผิดชอบต่อการละเมิดกฎได้อย่างไร?
- เอาล่ะ แล้วจะแก้ปัญหานี้อย่างไร เพราะโค้ดของฉันถูกเขียนโดยคำนึงถึงข้อควรระวังนี้อยู่แล้วและพยายามป้องกัน แต่ว่ามีส่วนประกอบบางอย่างที่อยู่เบื้องหลังยังคงสร้างคำเตือนอยู่
คอนโซลเบราว์เซอร์
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount
method.
in TextLayerInternal (created by Context.Consumer)
in TextLayer (created by PageInternal) index.js:1446
d/console[e]
index.js:1446
warningWithoutStack
react-dom.development.js:520
warnAboutUpdateOnUnmounted
react-dom.development.js:18238
scheduleWork
react-dom.development.js:19684
enqueueSetState
react-dom.development.js:12936
./node_modules/react/cjs/react.development.js/Component.prototype.setState
react.development.js:356
_callee$
TextLayer.js:97
tryCatch
runtime.js:63
invoke
runtime.js:282
defineIteratorMethods/</prototype[method]
runtime.js:116
asyncGeneratorStep
asyncToGenerator.js:3
_throw
asyncToGenerator.js:29
รหัส
Book.tsx
import { throttle } from 'lodash';
import * as React from 'react';
import { AutoWidthPdf } from '../shared/AutoWidthPdf';
import BookCommandPanel from '../shared/BookCommandPanel';
import BookTextPath from '../static/pdf/sde.pdf';
import './Book.css';
const DEFAULT_WIDTH = 140;
class Book extends React.Component {
setDivSizeThrottleable: () => void;
pdfWrapper: HTMLDivElement | null = null;
isComponentMounted: boolean = false;
state = {
hidden: true,
pdfWidth: DEFAULT_WIDTH,
};
constructor(props: any) {
super(props);
this.setDivSizeThrottleable = throttle(
() => {
if (this.isComponentMounted) {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
}
},
500,
);
}
componentDidMount = () => {
this.isComponentMounted = true;
this.setDivSizeThrottleable();
window.addEventListener("resize", this.setDivSizeThrottleable);
};
componentWillUnmount = () => {
this.isComponentMounted = false;
window.removeEventListener("resize", this.setDivSizeThrottleable);
};
render = () => (
<div className="Book">
{ this.state.hidden && <div className="Book__LoadNotification centered">Book is being loaded...</div> }
<div className={this.getPdfContentContainerClassName()}>
<BookCommandPanel
bookTextPath={BookTextPath}
/>
<div className="Book__PdfContent" ref={ref => this.pdfWrapper = ref}>
<AutoWidthPdf
file={BookTextPath}
width={this.state.pdfWidth}
onLoadSuccess={(_: any) => this.onDocumentComplete()}
/>
</div>
<BookCommandPanel
bookTextPath={BookTextPath}
/>
</div>
</div>
);
getPdfContentContainerClassName = () => this.state.hidden ? 'hidden' : '';
onDocumentComplete = () => {
try {
this.setState({ hidden: false });
this.setDivSizeThrottleable();
} catch (caughtError) {
console.warn({ caughtError });
}
};
}
export default Book;
AutoWidthPdf.tsx
import * as React from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
interface IProps {
file: string;
width: number;
onLoadSuccess: (pdf: any) => void;
}
export class AutoWidthPdf extends React.Component<IProps> {
render = () => (
<Document
file={this.props.file}
onLoadSuccess={(_: any) => this.props.onLoadSuccess(_)}
>
<Page
pageNumber={1}
width={this.props.width}
/>
</Document>
);
}
อัปเดต 1: ยกเลิกฟังก์ชันที่ปรับแรงบิดได้ (ยังไม่มีโชค)
const DEFAULT_WIDTH = 140;
class Book extends React.Component {
setDivSizeThrottleable: ((() => void) & Cancelable) | undefined;
pdfWrapper: HTMLDivElement | null = null;
state = {
hidden: true,
pdfWidth: DEFAULT_WIDTH,
};
componentDidMount = () => {
this.setDivSizeThrottleable = throttle(
() => {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
},
500,
);
this.setDivSizeThrottleable();
window.addEventListener("resize", this.setDivSizeThrottleable);
};
componentWillUnmount = () => {
window.removeEventListener("resize", this.setDivSizeThrottleable!);
this.setDivSizeThrottleable!.cancel();
this.setDivSizeThrottleable = undefined;
};
render = () => (
<div className="Book">
{ this.state.hidden && <div className="Book__LoadNotification centered">Book is being loaded...</div> }
<div className={this.getPdfContentContainerClassName()}>
<BookCommandPanel
BookTextPath={BookTextPath}
/>
<div className="Book__PdfContent" ref={ref => this.pdfWrapper = ref}>
<AutoWidthPdf
file={BookTextPath}
width={this.state.pdfWidth}
onLoadSuccess={(_: any) => this.onDocumentComplete()}
/>
</div>
<BookCommandPanel
BookTextPath={BookTextPath}
/>
</div>
</div>
);
getPdfContentContainerClassName = () => this.state.hidden ? 'hidden' : '';
onDocumentComplete = () => {
try {
this.setState({ hidden: false });
this.setDivSizeThrottleable!();
} catch (caughtError) {
console.warn({ caughtError });
}
};
}
export default Book;
คำตอบ #1
นี่คือวิธีแก้ปัญหาเฉพาะของ React Hooks สำหรับ
ข้อผิดพลาด
คำเตือน: ไม่สามารถอัปเดตสถานะของ React บนคอมโพเนนต์ที่ถูกยกเลิกการติดตั้งได้
วิธีแก้ปัญหา
คุณสามารถประกาศ let isMounted=true ภายใน useEffect ซึ่งจะถูกเปลี่ยนใน callback ทำความสะอาดทันทีที่ component ถูก unmounted ก่อนการอัปเดตสถานะ คุณสามารถตรวจสอบตัวแปรนี้ตามเงื่อนไขได้ดังนี้:
useEffect(() => {
let isMounted = true; // note mutable flag
someAsyncOperation().then(data => {
if (isMounted) setState(data); // add conditional check
})
return () => { isMounted = false }; // cleanup toggles value, if unmounted
}, []); // adjust dependencies to your needs
const Parent = () => {
const [mounted, setMounted] = useState(true);
return (
<div>
Parent:
<button onClick={() => setMounted(!mounted)}>
{mounted ? "Unmount" : "Mount"} Child
</button>
{mounted && <Child />}
<p>
Unmount Child, while it is still loading. It won't set state later on,
so no error is triggered.
</p>
</div>
);
};
const Child = () => {
const [state, setState] = useState("loading (4 sec)...");
useEffect(() => {
let isMounted = true;
fetchData();
return () => {
isMounted = false;
};
// simulate some Web API fetching
function fetchData() {
setTimeout(() => {
// drop "if (isMounted)" to trigger error again
// (take IDE, doesn't work with stack snippet)
if (isMounted) setState("data fetched")
else console.log("aborted setState on unmounted component")
}, 4000);
}
}, []);
return <div>Child: {state}</div>;
};
ReactDOM.render(<Parent />, document.getElementById("root"));<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>ส่วนขยาย: ฮุค useAsync แบบกำหนดเอง
เราสามารถรวมโค้ดซ้ำทั้งหมดเข้าไปใน Hook แบบกำหนดเอง ซึ่งจะยกเลิกฟังก์ชันแบบอะซิงโครนัสโดยอัตโนมัติในกรณีที่คอมโพเนนต์ถูกถอดออกหรือค่าของตัวแปรขึ้นอยู่มีการเปลี่ยนแปลงก่อน:
function useAsync(asyncFn, onSuccess) {
useEffect(() => {
let isActive = true;
asyncFn().then(data => {
if (isActive) onSuccess(data);
});
return () => { isActive = false };
}, [asyncFn, onSuccess]);
}
// custom Hook for automatic abortion on unmount or dependency change
// You might add onFailure for promise errors as well.
function useAsync(asyncFn, onSuccess) {
useEffect(() => {
let isActive = true;
asyncFn().then(data => {
if (isActive) onSuccess(data)
else console.log("aborted setState on unmounted component")
});
return () => {
isActive = false;
};
}, [asyncFn, onSuccess]);
}
const Child = () => {
const [state, setState] = useState("loading (4 sec)...");
useAsync(simulateFetchData, setState);
return <div>Child: {state}</div>;
};
const Parent = () => {
const [mounted, setMounted] = useState(true);
return (
<div>
Parent:
<button onClick={() => setMounted(!mounted)}>
{mounted ? "Unmount" : "Mount"} Child
</button>
{mounted && <Child />}
<p>
Unmount Child, while it is still loading. It won't set state later on,
so no error is triggered.
</p>
</div>
);
};
const simulateFetchData = () => new Promise(
resolve => setTimeout(() => resolve("data fetched"), 4000));
ReactDOM.render(<Parent />, document.getElementById("root"));<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>เพิ่มเติมเกี่ยวกับการเคลียร์ผลกระทบ: Overreacted: คู่มือสมบูรณ์สำหรับ useEffect
คำตอบ #2
เพื่อเอาออก - ไม่สามารถทำการอัปเดตสถานะของ React บนคอมโพเนนต์ที่ถูกยกเลิกการติดตั้งได้ ให้ใช้เมธอด componentDidMount ภายใต้เงื่อนไขและทำให้เงื่อนไขนั้นเป็นเท็จในเมธอด componentWillUnmount ตัวอย่างเช่น :-
class Home extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
news: [],
};
}
componentDidMount() {
this._isMounted = true;
ajaxVar
.get('https://domain')
.then(result => {
if (this._isMounted) {
this.setState({
news: result.data.hits,
});
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
...
}
}
คำตอบ #3
ถ้าวิธีแก้ปัญหาข้างต้นใช้ไม่ได้ ให้ลองวิธีนี้และมันใช้ได้ผลสำหรับฉัน:
componentWillUnmount() {
// fix Warning: Can't perform a React state update on an unmounted component
this.setState = (state,callback)=>{
return;
};
}
คำตอบ #4
มี hook ตัวหนึ่งที่ค่อนข้างทั่วไป มักถูกเรียกว่า useIsMounted ซึ่งแก้ปัญหานี้ได้ (สำหรับ functional components)...
import { useRef, useEffect } from 'react';
export function useIsMounted() {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => isMounted.current = false;
}, []);
return isMounted;
}
จากนั้นในคอมโพเนนต์ฟังก์ชันของคุณ
function Book() {
const isMounted = useIsMounted();
...
useEffect(() => {
asyncOperation().then(data => {
if (isMounted.current) { setState(data); }
})
});
...
}
คำตอบ #5
การตรวจสอบว่าคอมโพเนนต์ถูกติดตั้งหรือไม่จริง ๆ แล้วเป็นรูปแบบปฏิบัติที่ไม่แนะนำตามเอกสารของ React วิธีแก้ไขคำเตือน setState คือการใช้ประโยชน์จากการใช้ AbortController:
useEffect(() => {
const abortController = new AbortController() // creating an AbortController
fetch(url, { signal: abortController.signal }) // passing the signal to the query
.then(data => {
setState(data) // if everything went well, set the state
})
.catch(error => {
if (error.name === 'AbortError') return // if the query has been aborted, do nothing
throw error
})
return () => {
abortController.abort() // stop the query by aborting on the AbortController on unmount
}
}, [])
สำหรับการทำงานแบบอะซิงโครนัสที่ไม่ขึ้นอยู่กับ Fetch API ยังคงควรมีวิธีการยกเลิกการทำงานแบบอะซิงโครนัสเหล่านี้ และคุณควรใช้วิธีเหล่านี้มากกว่าการตรวจสอบเพียงว่าคอมโพเนนต์ถูกติดตั้งแล้วหรือไม่ หากคุณกำลังสร้าง API ของคุณเอง คุณสามารถใช้งาน AbortController API ในมันเพื่อจัดการกับมันได้
เพื่อให้เข้าใจบริบทมากขึ้น การตรวจสอบว่าคอมโพเนนต์ถูกติดตั้งแล้วหรือไม่เป็นแนวทางที่ไม่แนะนำ เนื่องจาก React กำลังตรวจสอบภายในว่าคอมโพเนนต์ถูกติดตั้งเพื่อแสดงคำเตือนนั้น การทำการตรวจสอบซ้ำอีกครั้งเป็นเพียงวิธีในการซ่อนคำเตือน และมีวิธีที่ง่ายกว่าบ้างในการซ่อนคำเตือนเหล่านั้นมากกว่าการเพิ่มโค้ดชิ้นนี้ในส่วนใหญ่ของฐานโค้ด
แหล่งที่มา: https://medium.com/doctolib/react-stop-checking-if-your-component-is-mounted-3bb2568a4934
คำตอบ #6
ฉันได้รับคำเตือนนี้อาจเป็นเพราะเรียกใช้ setState จาก effect hook (เรื่องนี้มีการพูดถึงในประเด็นทั้ง 3 ข้อที่เชื่อมโยงกัน)
อย่างไรก็ตาม การอัปเกรดเวอร์ชันของ React ทำให้คำเตือนหายไป
คำตอบ #7
React ได้ลบคำเตือนนี้แล้ว แต่ที่นี่มีวิธีแก้ปัญหาที่ดีกว่า (ไม่ใช่แค่ทางแก้ชั่วคราว)
useEffect(() => {
const abortController = new AbortController() // creating an AbortController
fetch(url, { signal: abortController.signal }) // passing the signal to the query
.then(data => {
setState(data) // if everything went well, set the state
})
.catch(error => {
if (error.name === 'AbortError') return // if the query has been aborted, do nothing
throw error
})
return () => {
abortController.abort()
}
}, [])
