27 Δεκ, 2021
React & useEffect cleanups
Πριν εξηγήσουμε τι είναι ένα useEffect
cleanup και πότε πρέπει να το χρησιμοποιήσουμε, ας ρίξουμε μια ματιά στον κώδικα παρακάτω. Παρατηρείτε κάτι που είναι λάθος;
import { useEffect, useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log("Increasing count...");
setCount((c) => c + 1);
}, 1000);
}, []);
return <h4>Count: {count}</h4>;
};
export default function App() {
const [showCounter, setShowCounter] = useState(false);
return (
<div>
<button onClick={() => setShowCounter((sc) => !sc)}>
Show/Hide Counter
</button>
{showCounter && <Counter />}
</div>
);
}
Τώρα ρίξτε μια ματιά στο παρακάτω demo και console αντίστοιχα:
Άρα, τί γίνεται εδώ;
- Κλικάρουμε το κουμπί για να δείξουμε τον Counter
- Το Counter component γίνεται mount και το interval ξεκινά
- Κλικάρουμε το κουμπί ξανά για να κρύψουμε τον Counter
- To Counter component γίνεται unmount αλλά το interval δε σταματάει ποτέ, συνεχίζει να τρέχει.
Οπότε παρά το γεγονός ότι το Counter
component γίνεται unmount αφού κλικάρουμε το κουμπί τη δεύτερη φορά, το interval που δημιουργήθηκε μέσα στη useEffect
του Counter
δε σταματά και τρέχει για πάντα προκαλώντας το παρακάτω error στην κονσόλα:
index.js:27 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 a useEffect cleanup function.
➡️ Το demo είναι διαθέσιμο εδώ: https://codesandbox.io/s/react-setinterval-memory-leak-4j60q
Τί είναι ένα useEffect cleanup;
Τις περισσότερες φορές όταν αναφερόμαστε σε ένα "useEffect cleanup" εννοούμε ότι χρειάζεται να γράψουμε κώδικα όπου θα μας εγγυηθεί ότι θα εξαλείψει οποιαδήποτε πιθανά memory leaks και ότι η εφαρμογή μας θα παραμείνει bug-free.
Ένα πιθανό memory leak, όπως είδαμε και στο προηγούμενο demo, μπορεί να είναι ότι δεν σταματήσαμε ένα interval (clear setInterval) όταν το component που δημιούργησε αυτο το interval έγινε unmount, και έτσι το interval συνεχίζει να τρέχει για πάντα! Ή θα μπορούσε να είναι ότι δεν κάναμε clean ένα subscription που δημιουργήσαμε και τώρα έχουμε ένα listener ο οποίος δεν απενεργοποιείται ποτέ!
Αυτές οι περιπτώσεις μπορούν πολύ εύκολα να εισάγουν τρομερά bugs στην εφαρμογή μας και παρόλο που τα cleanups σε ένα useEffect δεν είναι πάντα απαραίτητα, πρέπει πάντα όταν γράφουμε ένα useEffect να κάνουμε ερωτήσεις στον εαυτό μας όπως: "Έάν το component γίνει unmount, θα συνεχίσει να τρέχει ανεπιθύμητος κώδικας;" ή "Χρειάζεται να γράψουμε συνάρτηση cleanup για αυτό το effect;".
Πώς να γράψουμε ένα cleanup;
Το να γράψουμε ένα useEffect
cleanup είναι πολύ εύκολο. Απλώς επιστρέφουμε μια συνάρτηση από το useEffect
μας όπως φαίνεται παρακάτω:
useEffect(()=> {
// our effect here...
// we return the cleanup fn
return () => {
// our cleanup code here..
}
}, []);
Κάθε πότε τρέχουν τα cleanups;
Η React εφαρμόζει το cleanup όταν το component γίνεται unmount. Όμως, εξαιτίας του γεγονότος ότι τα effects μας μπορεί να τρέξουν και άλλες φορές σε πολλά renders (και όχι μόνο κατα το mount), η React καθαρίζει (cleans up) τα effects από το previous render πριν τρέξει τα effects την επόμενη φορά. Μην ανησυχείτε εάν είναι δυσνόητο για την ώρα - θα το καταλάβουμε πολύ καλύτερα με τα παραδείγματα παρακάτω.
Παραδείγματα Cleanup
Ας διορθώσουμε το counter demo από πριν και ας εισάγουμε μια συνάρτηση cleanup η οποία θα τρέχει όταν το Counter
component γίνεται unmount και θα σταματάει το interval:
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
let interval;
interval = setInterval(() => {
console.log("Increasing count...");
setCount((c) => c + 1);
}, 1000);
// our cleanup will run when the component unmounts
return () => {
clearInterval(interval);
}
}, []);
return <h4>Count: {count}</h4>;
};
Άρα γενικά μιλώντας ένα subscription μπορεί να δημιουργηθεί και να καταστραφεί σε ένα useEffect κάπως έτσι:
const Subscription = () => {
useEffect(() => {
createSubscription();
// our cleanup
return () => {
destroySubscription();
}
}, []);
return <div>...</div>
};
Η React καθαρίζει (cleans up) τα effects από το previous render πριν τρέξει τα effects την επόμενη φορά.
Βάσει της πρότασης παραπάνω, ας δούμε το παράδειγμα που ακολουθεί:
const FriendStatus = (props) => {
const { friendId } = props;
useEffect(() => {
// ...
subscribeToFriendStatus(friendId, handleStatusChange);
}, [friendId]);
return <div>...</div>
};
Το παραπάνω component FriendStatus
παίρνει ένα friendId
σαν prop και κάνει subscribe στην κατάσταση (status) ενός φίλου με το συγκεκριμένο friendId
, το οποίο σημαίνει ότι κάθε φορά που η κατάσταση αλλάζει εμείς πρέπει να εκτελούμε μια συνάρτηση όπου για τους σκοπούς αυτού του demo ονομάσαμε ως handleStatusChange
.
To useEffect
θα τρέξει μια φορά στο mount και μετά κάθε φορά που το friendId
αλλάζει (καθώς περάσαμε το friendId
στη λίστα των dependencies του useEffect
)
Οπότε φανταστείτε το σενάριο και την ροή παρακάτω:
- Τρέξε το πρώτο effect: Mount με
friendId:1
->subscribeToFriendStatus(1, handleStatusChange)
(το friendId αλλάζει)
2. Τρέξε το επόμενο effect: Update με friendId: 2
-> subscribeToFriendStatus(2, handleStatusChange)
(το friendId αλλάζει)
- Τρέξε το επόμενο effect: Update με
friendId: 3
->subscribeToFriendStatus(3, handleStatusChange)
Το πρόβλημα εδώ είναι ότι ποτέ δεν κάνουμε unsusbscribe από το προηγούμενο friendId
πριν ξανα κάνουμε subscribe στο καινούργιο friendId
και αυτό σημαίνει ότι καταλήγουμε να έχουμε όλα αυτά τα ανοιχτά subscriptions για όλα αυτά τα friendIds!
Προσθέτοντας μια cleanup συνάρτηση όπως φαίνεται παρακάτω, η React θα την εκτελεί πριν τρέξει τα effects στο επόμενο render (και φυσικά τελικά στο unmount).
const FriendStatus = (props) => {
const {friendId} = props;
useEffect(() => {
// ...
subscribeToFriendStatus(friendId, handleStatusChange);
return () => {
unSubscribeToFriendStatus(friendId, handleStatusChange);
}
}, [friendId]);
return <div>...</div>
};
Άρα τώρα η ροή γίνεται:
- Τρέξε το πρώτο effect: Mount με
friendId: 1
->subscribeToFriendStatus(1, handleStatusChange)
(το friendId αλλάζει)
- Καθάρισε το προηγούμενο effect: Unsubscribe από
friendId: 1
->unSubscribeToFriendStatus(1, handleStatusChange)
- Τρέξε το επόμενο effect: Update με
friendId: 2
->subscribeToFriendStatus(2, handleStatusChange)
(το friendId αλλάζει)
- Καθάρισε το προηγούμενο effect: Unsubscribe από
friendId: 2
->unSubscribeToFriendStatus(2, handleStatusChange)
- Τρέξε το επόμενο effect: Update με
friendId: 3
->subscribeToFriendStatus(3, handleStatusChange)
(το component γίνεται unmount)
- Καθάρισε το προηγούμενο effect: Unsubscribe από
friendId: 3
->unSubscribeToFriendStatus(3, handleStatusChange)
Τέλος
Ελπίζω να σας άρεσε και να σας βοήθησε αυτό το άρθρο. Τα λέμε μετά! 😀
Εγγραφή
Εγγραφειτε στην λιστα
Εγγραφείτε με το e-mail σας για να σας στέλνω το υλικό μου. Δεν θα είναι spam, σας το υπόσχομαι! Μπορείτε να καταργήσετε την εγγραφή σας όποτε θέλετε.