Πώς να συνεργαστείτε με το React με τον σωστό τρόπο για να αποφύγετε κάποιες κοινές παγίδες

Πληκτρολόγιο Macbook Pro

Ένα πράγμα που ακούω αρκετά συχνά είναι " Ας πάμε για Redux " στη νέα μας εφαρμογή React. Σας βοηθά να κλιμακώσετε και τα δεδομένα της εφαρμογής δεν πρέπει να βρίσκονται στην τοπική κατάσταση του React επειδή είναι αναποτελεσματικά. Ή όταν καλείτε ένα API και ενώ η υπόσχεση εκκρεμεί, το στοιχείο αποσυνδέεται και λαμβάνετε το ακόλουθο όμορφο σφάλμα.

Προειδοποίηση: Δεν είναι δυνατή η κλήση του setState (ή forceUpdate) σε ένα στοιχείο που δεν έχει τοποθετηθεί. Αυτό δεν είναι op, αλλά δείχνει διαρροή μνήμης στην εφαρμογή σας. Για διόρθωση, ακυρώστε όλες τις συνδρομές και ασύγχρονες εργασίες στη μέθοδο komponenWillUnmount.

Έτσι, η λύση που συνήθως φτάνουν οι άνθρωποι είναι η χρήση του Redux .Λατρεύω τον Redux και το έργο που κάνει ο Dan Abramov είναι απλώς απίστευτο! Αυτό το μάγκα λικνίζει πολύ - εύχομαι να ήμουν τόσο ταλαντούχος όσο αυτός.

Αλλά είμαι βέβαιος ότι όταν ο Dan έκανε τον Redux, μας έδωσε ένα εργαλείο στη ζώνη εργαλείων μας ως βοηθός. Δεν είναι ο Jack όλων των εργαλείων. Δεν χρησιμοποιείτε σφυρί όταν μπορείτε να βιδώσετε το μπουλόνι με ένα κατσαβίδι.

Ο Dan συμφωνεί ακόμη .

Λατρεύω το React και το δουλεύω σχεδόν δύο χρόνια τώρα. Μέχρι στιγμής, δεν μετανιώνω. Η καλύτερη απόφαση ποτέ. Μου αρέσει ο Vue και όλα τα ωραία βιβλιοθήκη / πλαίσια εκεί έξω. Αλλά η React κατέχει μια ξεχωριστή θέση στην καρδιά μου. Με βοηθά να επικεντρωθώ στη δουλειά που υποθέτω ότι πρέπει να κάνω αντί να αφιερώνω όλο το χρόνο μου σε χειρισμούς DOM. Και το κάνει με τον καλύτερο και αποτελεσματικότερο δυνατό τρόπο. με την αποτελεσματική συμφιλίωσή του.

Έχω μάθει πολλά τα τελευταία χρόνια και έχω παρατηρήσει ένα κοινό πρόβλημα μεταξύ των νέων και έμπειρων προγραμματιστών του React: να μην χρησιμοποιώ το React με τον σωστό τρόπο όταν χειρίζεστε συνδρομές ή ασύγχρονες εργασίες. Πιστεύω ότι η τεκμηρίωση εκεί έξω δεν είναι καλά τοποθετημένη σε αυτήν την περίπτωση, και έτσι αποφάσισα να γράψω αυτό το άρθρο.

Θα μιλήσω πρώτα για τις συνδρομές και, στη συνέχεια, θα προχωρήσουμε στον χειρισμό ασύγχρονης ακύρωσης εργασιών για την αποφυγή διαρροών μνήμης στο React (ο κύριος σκοπός αυτού του άρθρου). Εάν δεν αντιμετωπιστεί, αυτό επιβραδύνει την εφαρμογή μας.

Τώρα ας επιστρέψουμε σε αυτό το όμορφο μήνυμα σφάλματος για το οποίο συζητήσαμε αρχικά:

Προειδοποίηση: Δεν είναι δυνατή η κλήση του setState (ή forceUpdate) σε ένα στοιχείο που δεν έχει τοποθετηθεί. Αυτό δεν είναι op, αλλά δείχνει διαρροή μνήμης στην εφαρμογή σας. Για διόρθωση, ακυρώστε όλες τις συνδρομές και ασύγχρονες εργασίες στη μέθοδο komponenWillUnmount.

Ο στόχος μου για αυτό το άρθρο είναι να βεβαιωθώ ότι κανείς δεν πρέπει ποτέ να αντιμετωπίσει αυτό το σφάλμα και να μην ξέρει τι να κάνει ξανά για αυτό.

Τι θα καλύψουμε

  • Εκκαθάριση συνδρομών όπως το setTimeout / setInterval
  • Διαγράψτε ασύγχρονες ενέργειες όταν καλείτε ένα αίτημα XHR χρησιμοποιώντας fetchή βιβλιοθήκες όπωςaxios
  • Εναλλακτικές μέθοδοι, μερικές θεωρούν άλλες καταργήθηκαν.

Πριν ξεκινήσω, μια τεράστια κραυγή στο Kent C Dodds , το πιο ωραίο άτομο στο Διαδίκτυο αυτή τη στιγμή. Σας ευχαριστούμε που αφιερώσατε χρόνο και δώσατε πίσω στην κοινότητα. Τα podcast του στο Youtubeκαιegghead μάθημα για Advanced React Component Patterns είναι καταπληκτικά. Ελέγξτε αυτούς τους πόρους εάν θέλετε να κάνετε το επόμενο βήμα στις δεξιότητές σας στο React.

Ρώτησα τον Κεντ για μια καλύτερη προσέγγιση για να αποφύγω το setState στο στοιχείο unmount, έτσι θα μπορούσα να βελτιστοποιήσω καλύτερα την απόδοση του React Πήγε πάνω και πέρα ​​και έκανε ένα βίντεο σε αυτό. Εάν είστε είδος βίντεο, δείτε το παρακάτω. Θα σας δώσει βήμα προς βήμα μια λεπτομερή εξήγηση.

Ας ξεκινήσουμε λοιπόν για να ξεκινήσουμε.

1: Εκκαθάριση συνδρομών

Ας ξεκινήσουμε με το παράδειγμα:

Ας μιλήσουμε τι συνέβη εδώ. Αυτό που θέλω να εστιάσετε είναι το counter.jsαρχείο που ουσιαστικά αυξάνει τον μετρητή μετά από 3 δευτερόλεπτα.

Αυτό δίνει σφάλμα σε 5 δευτερόλεπτα, επειδή αποσυνδέω μια συνδρομή χωρίς να την εκκαθαρίσω. Εάν θέλετε να δείτε ξανά το σφάλμα, απλώς πατήστε το κουμπί ανανέωσης στο πρόγραμμα επεξεργασίας CodeSandbox για να δείτε το σφάλμα στην κονσόλα.

Έχω το αρχείο κοντέινερ μου index.jsπου απλώς εναλλάσσει το αντίθετο στοιχείο μετά τα πρώτα πέντε δευτερόλεπτα.

Έτσι

- - - → Index.js— - - - → Counter.js

Στο Index.js μου, καλώ το Counter.js και το κάνω απλώς στην απόδοση μου:

{showCounter ?  : null}

Η showCounterκατάσταση είναι boolean που ορίζεται ως ψευδής μετά τα πρώτα 5 δευτερόλεπτα μόλις η συνάρτηση αναρτηθεί (componentDidMount).

Το πραγματικό πράγμα που δείχνει το πρόβλημά μας εδώ είναι το counter.jsαρχείο που αυξάνει τον αριθμό μετά από κάθε 3 δευτερόλεπτα. Έτσι, μετά τα πρώτα 3 δευτερόλεπτα, ο μετρητής ενημερώνεται. Αλλά μόλις φτάσει στη δεύτερη ενημέρωση, η οποία συμβαίνει στις 6ηΔεύτερον, το index.jsαρχείο έχει ήδη αποσυνδέσει το στοιχείο του μετρητή στο 5οδεύτερος. Μέχρι τη στιγμή που το στοιχείο μετρητή φτάσει στο 6οΔεύτερον, ενημερώνει τον μετρητή για δεύτερη φορά.

Ενημερώνει την κατάστασή του, αλλά εδώ είναι το πρόβλημα. Δεν υπάρχει DOM για το στοιχείο μετρητή για ενημέρωση της κατάστασης και, όταν το React ρίχνει ένα σφάλμα. Αυτό το όμορφο σφάλμα που συζητήσαμε παραπάνω:

Προειδοποίηση: Δεν είναι δυνατή η κλήση του setState (ή forceUpdate) σε ένα στοιχείο που δεν έχει τοποθετηθεί. Αυτό δεν είναι op, αλλά δείχνει διαρροή μνήμης στην εφαρμογή σας. Για διόρθωση, ακυρώστε όλες τις συνδρομές και ασύγχρονες εργασίες στη μέθοδο komponenWillUnmount.

Τώρα αν είστε νέοι στο React, θα μπορούσατε να πείτε, "καλά Adeel ... ναι, αλλά δεν αποπροσαρμόσαμε απλώς το στοιχείο Counter στο 5ο δευτερόλεπτο; Εάν δεν υπάρχει στοιχείο για μετρητή, πώς μπορεί η κατάσταση να εξακολουθεί να ενημερώνεται στο έκτο δευτερόλεπτο; "

Ναι έχεις δίκιο. Αλλά όταν κάνουμε κάτι σαν setTimeoutή setIntervalστα συστατικά του React, δεν εξαρτάται ή δεν συνδέεται με την τάξη React όπως νομίζετε ότι μπορεί να είναι. Θα συνεχίσει να λειτουργεί μετά την καθορισμένη του κατάσταση, εκτός εάν ή μέχρι να ακυρώσετε τη συνδρομή της.

Τώρα ίσως το κάνετε ήδη όταν πληρούται η κατάστασή σας. Τι γίνεται όμως αν η κατάστασή σας δεν έχει ακόμη εκπληρωθεί και ο χρήστης αποφασίσει να αλλάξει σελίδες όπου αυτή η ενέργεια εξακολουθεί να συμβαίνει;

Ο καλύτερος τρόπος για να διαγράψετε τέτοιου είδους συνδρομές είναι στον componentWillUnmountκύκλο ζωής σας . Εδώ είναι ένα παράδειγμα πώς μπορείτε να το κάνετε. Δείτε τη μέθοδο componentWillUnmount του αρχείου counter.js:

Και αυτό είναι αρκετά για setTimout& setInterval.

2: Απορρίψεις API (XHR)

  • Η άσχημη παλιά προσέγγιση (Καταργήθηκε)
  • Η Καλή Νεότερη Προσέγγιση (Ο κύριος σκοπός αυτού του άρθρου)

So, we’ve discussed subscriptions. But what if you make an asynchronous request? How do you cancel it?

The old way

Before I talk about that, I want to talk about a deprecated method in React called isMounted()

Before December 2015, there was a method called isMounted in React. You can read more about it in the React blog. What it did was something like this:

import React from 'react' import ReactDOM from 'react-dom' import axios from 'axios' class RandomUser extends React.Component { state = {user: null} _isMounted = false handleButtonClick = async () => { const response = await axios.get('//randomuser.me/api/') if (this._isMounted) { this.setState({ user: response.data }) } } componentDidMount() { this._isMounted = true } componentWillUnmount() { this._isMounted = false } render() { return ( Click Me 
{JSON.stringify(this.state.user, null, 2)}
) } }

For the purpose of this example, I am using a library called axios for making an XHR request.

Let’s go through it. I initially set this_isMounted to false right next to where I initialized my state. As soon as the life cycle componentDidMount gets called, I set this._isMounted to true. During that time, if an end user clicks the button, an XHR request is made. I am using randomuser.me. As soon as the promise gets resolved, I check if the component is still mounted with this_isMounted. If it’s true, I update my state, otherwise I ignore it.

The user might clicked on the button while the asynchronous call was being resolved. This would result in the user switching pages. So to avoid an unnecessary state update, we can simply handle it in our life cycle method componentWillUnmount. I simply set this._isMounted to false. So whenever the asynchronous API call gets resolved, it will check if this_isMounted is false and then it will not update the state.

Αυτή η προσέγγιση κάνει τη δουλειά, αλλά όπως λένε τα έγγραφα React:

Η κύρια περίπτωση χρήσης isMounted()είναι να αποφεύγεται η κλήση setState()μετά την αποσύνδεση ενός στοιχείου, επειδή η κλήση setState()μετά την αποσύνδεση ενός στοιχείου θα εκπέμπει μια προειδοποίηση. Η "προειδοποίηση setState" υπάρχει για να σας βοηθήσει να εντοπίσετε σφάλματα, επειδή η κλήση setState()ενός στοιχείου που δεν έχει τοποθετηθεί είναι μια ένδειξη ότι η εφαρμογή / στοιχείο σας κατά κάποιον τρόπο απέτυχε να καθαρίσει σωστά. Συγκεκριμένα, η κλήση setState()σε ένα στοιχείο που δεν έχει τοποθετηθεί σημαίνει ότι η εφαρμογή σας εξακολουθεί να διατηρεί αναφορά στο στοιχείο μετά την αποσύνδεση του στοιχείου - κάτι που υποδηλώνει συχνά διαρροή μνήμης! Διαβάστε περισσότερα …

Αυτό σημαίνει ότι αν και αποφύγαμε ένα περιττό setState, η μνήμη δεν έχει ακόμη εκκαθαριστεί. Υπάρχει ακόμα μια ασύγχρονη ενέργεια που δεν γνωρίζει ότι ο κύκλος ζωής των συστατικών έχει τελειώσει και δεν χρειάζεται πλέον.

Ας μιλήσουμε για τον σωστό τρόπο

Εδώ για να σώσετε την ημέρα είναι AbortControllers . Σύμφωνα με την τεκμηρίωση MDN αναφέρει:

Η AbortControllerδιεπαφή αντιπροσωπεύει ένα αντικείμενο ελεγκτή που σας επιτρέπει να ακυρώσετε ένα ή περισσότερα αιτήματα DOM όπως και όταν θέλετε. Διαβάστε περισσότερα ..

Ας δούμε λίγο περισσότερο σε βάθος εδώ. Με κωδικό, φυσικά, γιατί όλοι ❤ κωδικοί.

var myController = new AbortController(); var mySignal = myController.signal; var downloadBtn = document.querySelector('.download'); var abortBtn = document.querySelector('.abort'); downloadBtn.addEventListener('click', fetchVideo); abortBtn.addEventListener('click', function() { myController.abort(); console.log('Download aborted'); }); function fetchVideo() { ... fetch(url, { signal: mySignal }).then(function(response) { ... }).catch(function(e) { reports.textContent = 'Download error: ' + e.message; }) } 

First we create a new AbortController and assign it to a variable called myController. Then we make a signal for that AbortController. Think of the signal as an indicator to tell our XHR requests when it’s time to abort the request.

Assume that we have 2 buttons, Download and Abort . The download button downloads a video, but what if, while downloading, we want to cancel that download request? We simply need to call myController.abort(). Now this controller will abort all requests associated with it.

How, you might ask?

After we did var myController = new AbortController() we did this var mySignal = myController.signal . Now in my fetch request, where I tell it the URL and the payload, I just need to pass in mySignal to link/signal that FETCh request with my awesome AbortController.

Αν θέλετε να διαβάσετε ένα ακόμη πιο εκτεταμένο παράδειγμα AbortController, οι υπέροχοι λαοί στο MDN έχουν αυτό το πραγματικά ωραίο και κομψό παράδειγμα στο Github τους. Μπορείτε να το δείτε εδώ.

Ήθελα να μιλήσω για αυτά τα αιτήματα ματαίωσης επειδή δεν γνωρίζουν πολλά άτομα. Το αίτημα για ματαίωση στην ανάκτηση ξεκίνησε το 2015. Ακολουθεί το αρχικό ζήτημα GitHub On Abort - τελικά έλαβε υποστήριξη γύρω στον Οκτώβριο του 2017. Αυτό είναι ένα κενό δύο ετών. Ουάου! Υπάρχουν μερικές βιβλιοθήκες όπως τα axios που παρέχουν υποστήριξη για το AbortController. Θα συζητήσω πώς μπορείτε να το χρησιμοποιήσετε με άξονες, αλλά πρώτα ήθελα να δείξω την αναλυτική έκδοση του τρόπου λειτουργίας του AbortController.

Ματαίωση ενός αιτήματος XHR στο Axios

«Κάνε ή όχι. Δεν υπαρχει καμια προσπαθεια." - Γιόντα

The implementation I talked about above isn’t specific to React, but that’s what we’ll discuss here. The main purpose of this article is to show you how to clear unnecessary DOM manipulations in React when an XHR request is made and the component is unmounted while the request is in pending state. Whew!

So without further ado, here we go.

import React, { Component } from 'react'; import axios from 'axios'; class Example extends Component { signal = axios.CancelToken.source(); state = { isLoading: false, user: {}, } componentDidMount() { this.onLoadUser(); } componentWillUnmount() { this.signal.cancel('Api is being canceled'); } onLoadUser = async () => { try { this.setState({ isLoading: true }); const response = await axios.get('//randomuser.me/api/', { cancelToken: this.signal.token, }) this.setState({ user: response.data, isLoading: true }); } catch (err) { if (axios.isCancel(err)) { console.log('Error: ', err.message); // => prints: Api is being canceled } else { this.setState({ isLoading: false }); } } } render() { return ( 
{JSON.stringify(this.state.user, null, 2)}
) } }

Let’s walk through this code

I set this.signal to axios.CancelToken.source()which basically instantiates a new AbortController and assigns the signal of that AbortController to this.signal. Next I call a method in componentDidMount called this.onLoadUser() which calls a random user information from a third party API randomuser.me. When I call that API, I also pass the signal to a property in axios called cancelToken

The next thing I do is in my componentWillUnmount where I call the abort method which is linked to that signal. Now let’s assume that as soon as the component was loaded, the API was called and the XHR request went in a pending state.

Now, the request was pending (that is, it wasn’t resolved or rejected but the user decided to go to another page. As soon as the life cycle method componentWillUnmount gets called up, we will abort our API request. As soon as the API get’s aborted/cancelled, the promise will get rejected and it will land in the catch block of that try/catch statement, particularly in the if (axios.isCancel(err) {} block.

Now we know explicitly that the API was aborted, because the component was unmounted and therefore logs an error. But we know that we no longer need to update that state since it is no longer required.

P.S: You can use the same signal and pass it as many XHR requests in your component as you like. When the component gets un mounted, all those XHR requests that are in a pending state will get cancelled when componentWillUnmount is called.

Final details

Congratulations! :) If you have read this far, you’ve just learned how to abort an XHR request on your own terms.

Let’s carry on just a little bit more. Normally, your XHR requests are in one file, and your main container component is in another (from which you call that API method). How do you pass that signal to another file and still get that XHR request cancelled?

Here is how you do it:

import React, { Component } from 'react'; import axios from 'axios'; // API import { onLoadUser } from './UserAPI'; class Example extends Component { signal = axios.CancelToken.source(); state = { isLoading: false, user: {}, } componentDidMount() { this.onLoadUser(); } componentWillUnmount() { this.signal.cancel('Api is being canceled'); } onLoadUser = async () => { try { this.setState({ isLoading: true }); const data = await onLoadUser(this.signal.token); this.setState({ user: data, isLoading: true }); } catch (error) { if (axios.isCancel(err)) { console.log('Error: ', err.message); // => prints: Api is being canceled } else { this.setState({ isLoading: false }); } } } render() { return ( 
{JSON.stringify(this.state.user, null, 2)}
) } }; }
export const onLoadUser = async myCancelToken => { try { const { data } = await axios.get('//randomuser.me/api/', { cancelToken: myCancelToken, }) return data; } catch (error) { throw error; } }; 

I hope this has helped you and I hope you’ve learned something. If you liked it, please give it some claps.

Σας ευχαριστούμε που αφιερώσατε χρόνο για να διαβάσετε. Φώναξε στον πολύ ταλαντούχο συνάδελφό μου Kinan που με βοήθησε να διαβάσω αυτό το άρθρο. Ευχαριστώ τον Kent C Dodds που εμπνέει την κοινότητα JavaScript OSS.

Και πάλι, θα ήθελα να ακούσω τα σχόλιά σας σχετικά με αυτό. Μπορείτε πάντα να επικοινωνήσετε μαζί μου στο Twitter .

Επίσης, υπάρχει μια άλλη καταπληκτική ανάγνωση στο Abort Controller που βρήκα μέσω της τεκμηρίωσης MDN από τον Jake Archibald . Σας προτείνω να το διαβάσετε, αν έχετε μια περίεργη φύση όπως η δική μου.