Το λειτουργικό setState είναι το μέλλον του React

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

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

Όμως η ομάδα του React απέχει πολύ από το να παραιτηθεί. Συνεχίζουν να σκάβουν βαθύτερα, ανακαλύπτοντας ακόμη πιο λειτουργικούς πολύτιμους λίθους κρυμμένους στη θρυλική βιβλιοθήκη.

Έτσι, σήμερα σας αποκαλύπτω ένα νέο λειτουργικό χρυσό που θάβεται στο React, το καλύτερα διατηρημένο μυστικό του React - Functional setState!

Εντάξει, μόλις έφτιαξα αυτό το όνομα… και δεν είναι εντελώς νέο ή μυστικό. Όχι, όχι ακριβώς. Βλέπετε, είναι ένα μοτίβο ενσωματωμένο στο React, το οποίο είναι γνωστό μόνο από λίγους προγραμματιστές που έχουν πραγματικά σκάψει βαθιά. Και δεν είχε ποτέ όνομα. Αλλά τώρα το κάνει - Λειτουργικό setState!

Ακολουθώντας τα λόγια του Dan Abramov στην περιγραφή αυτού του μοτίβου, το Functional setState είναι ένα μοτίβο όπου εσείς

"Δήλωση αλλαγών κατάστασης ξεχωριστά από τις κλάσεις στοιχείων."

Ε;

Εντάξει… αυτό που ήδη γνωρίζετε

Το React είναι μια βιβλιοθήκη UI που βασίζεται σε στοιχεία. Ένα στοιχείο είναι βασικά μια συνάρτηση που αποδέχεται ορισμένες ιδιότητες και επιστρέφει ένα στοιχείο διεπαφής χρήστη.

function User(props) { return ( A pretty user );}

Ένα στοιχείο μπορεί να χρειαστεί να έχει και να διαχειρίζεται την κατάστασή του. Σε αυτήν την περίπτωση, συνήθως γράφετε το στοιχείο ως τάξη. Στη συνέχεια, έχετε την κατάσταση ζωντανή στη constructorλειτουργία τάξης :

class User { constructor () { this.state = { score : 0 }; }
 render () { return ( This user scored {this.state.score} ); }}

Για τη διαχείριση της κατάστασης, το React παρέχει μια ειδική μέθοδο που ονομάζεται setState(). Το χρησιμοποιείτε έτσι:

class User { ... 
 increaseScore () { this.setState({score : this.state.score + 1}); }
 ...}

Σημειώστε πώς setState()λειτουργεί. Το μεταβιβάζετε σε ένα αντικείμενο που περιέχει τμήματα της κατάστασης που θέλετε να ενημερώσετε. Με άλλα λόγια, το αντικείμενο που περνάτε θα έχει κλειδιά που αντιστοιχούν στα πλήκτρα στην κατάσταση του στοιχείου και στη συνέχεια setState()ενημερώνει ή ορίζει την κατάσταση συγχωνεύοντας το αντικείμενο με την κατάσταση. Έτσι, «set-State».

Αυτό που μάλλον δεν ήξερες

Θυμάστε πώς είπαμε ότι setState()λειτουργεί; Λοιπόν, τι θα σας έλεγα ότι αντί να περάσετε ένα αντικείμενο, θα μπορούσατε να περάσετε μια συνάρτηση ;

Ναί. setState()αποδέχεται επίσης μια συνάρτηση. Η συνάρτηση αποδέχεται την προηγούμενη κατάσταση και τα τρέχοντα στηρίγματα του στοιχείου που χρησιμοποιεί για τον υπολογισμό και την επιστροφή της επόμενης κατάστασης. Δείτε το παρακάτω:

this.setState(function (state, props) { return { score: state.score - 1 }});

Σημειώστε ότι setState()είναι μια συνάρτηση και μεταβιβάζουμε μια άλλη συνάρτηση σε αυτήν (λειτουργικός προγραμματισμός… λειτουργικό setState ). Με την πρώτη ματιά, αυτό μπορεί να φαίνεται άσχημο, πάρα πολλά βήματα μόνο για να ορίσετε. Γιατί θα θέλατε ποτέ να το κάνετε αυτό;

Γιατί να περάσετε μια συνάρτηση στο setState?

Το θέμα είναι, οι ενημερώσεις κατάστασης μπορεί να είναι ασύγχρονες.

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

Μπά! Τόση πολλή δουλειά! Στην πραγματικότητα, αυτή είναι ακόμη και μια υπερβολικά απλοποιημένη περίληψη. Αλλά εμπιστευτείτε το React!

Το React δεν απλώς «set-state».

Λόγω του όγκου των εργασιών, η κλήση setState()ενδέχεται να μην ενημερώσει αμέσως την κατάστασή σας.

Το React μπορεί να περιλαμβάνει πολλές setState()κλήσεις σε μία ενημέρωση για απόδοση.

Τι σημαίνει αυτό το React;

Πρώτον, οι " πολλαπλές setState()κλήσεις" θα μπορούσαν να σημαίνουν κλήσεις setState()μέσα σε μία λειτουργία περισσότερες από μία φορές, ως εξής:

...
state = {score : 0};
// multiple setState() callsincreaseScoreBy3 () { this.setState({score : this.state.score + 1}); this.setState({score : this.state.score + 1}); this.setState({score : this.state.score + 1});}
...

Τώρα, όταν το React, συναντά « πολλαπλές setState()κλήσεις», αντί να κάνει αυτό το «set-state» τρεις φορές , το React θα αποφύγει αυτό το τεράστιο έργο που περιέγραψα παραπάνω και λέω έξυπνα στον εαυτό του: «Όχι! Δεν πρόκειται να ανέβω αυτό το βουνό τρεις φορές, να μεταφέρω και να ενημερώσω κάποια κατάσταση σε κάθε ταξίδι. Όχι, θα προτιμούσα να πάρω ένα κοντέινερ, να συσκευάσω όλα αυτά τα κομμάτια μαζί και να κάνω αυτήν την ενημέρωση μόνο μία φορά. " Και αυτό είναι οι φίλοι μουπαρτίδα !

Να θυμάστε ότι αυτό που μεταβιβάζετε setState()είναι ένα απλό αντικείμενο. Τώρα, ας υποθέσουμε ότι το React αντιμετωπίζει " πολλαπλές setState()κλήσεις", κάνει την παρτίδα εξάγοντας όλα τα αντικείμενα που μεταβιβάζονται σε κάθε setState()κλήση, τα συγχωνεύει μαζί για να σχηματίσει ένα μόνο αντικείμενο και, στη συνέχεια, χρησιμοποιεί αυτό το μεμονωμένο αντικείμενο setState().

Στο JavaScript, η συγχώνευση αντικειμένων μπορεί να μοιάζει με αυτό:

const singleObject = Object.assign( {}, objectFromSetState1, objectFromSetState2, objectFromSetState3);

Αυτό το μοτίβο είναι γνωστό ως σύνθεση αντικειμένου.

Στο JavaScript, ο τρόπος "συγχώνευσης" ή σύνθεσης αντικειμένων είναι: εάν τα τρία αντικείμενα έχουν τα ίδια πλήκτρα, η τιμή του κλειδιού του τελευταίου αντικειμένου μεταφέρεται σε Object.assign()νίκες. Για παράδειγμα:

const me = {name : "Justice"}, you = {name : "Your name"}, we = Object.assign({}, me, you);
we.name === "Your name"; //true
console.log(we); // {name : "Your name"}

Because you are the last object merged into we, the value of name in the you object — “Your name” — overrides the value of name in the me object. So “Your name” makes it into the we object… you win! :)

Thus, if you call setState() with an object multiple times — passing an object each time — React will merge. Or in other words, it will compose a new object out of the multiple objects we passed it. And if any of the objects contains the same key, the value of the key of the last object with same key is stored. Right?

Αυτό σημαίνει ότι, δεδομένης της increaseScoreBy3λειτουργίας μας παραπάνω, το τελικό αποτέλεσμα της συνάρτησης θα είναι μόνο 1 αντί για 3, επειδή το React δεν ενημέρωσε αμέσως την κατάσταση με τη σειρά που καλέσαμε setState(). Αλλά πρώτα, το React συνέθεσε όλα τα αντικείμενα μαζί, το οποίο έχει ως αποτέλεσμα: {score : this.state.score + 1}και στη συνέχεια το "set-state" μόνο μία φορά - με το νέο αντικείμενο. Κάτι σαν αυτό: User.setState({score : this.state.score + 1}.

Για να είμαι εξαιρετικά ξεκάθαρος, η μεταφορά αντικειμένου setState()δεν είναι το πρόβλημα εδώ. Το πραγματικό πρόβλημα είναι η μεταφορά αντικειμένου στο setState()οποίο θέλετε να υπολογίσετε την επόμενη κατάσταση από την προηγούμενη κατάσταση. Σταματήστε λοιπόν να το κάνετε αυτό. Δεν είναι ασφαλές!

Επειδή this.propsκαι this.stateμπορεί να ενημερωθεί ασύγχρονα, δεν πρέπει να βασίζεστε στις τιμές τους για τον υπολογισμό της επόμενης κατάστασης.

Εδώ είναι ένα στυλό της Sophia Shoemaker που επιδεικνύει αυτό το πρόβλημα. Παίξτε με αυτό και δώστε προσοχή τόσο στις κακές όσο και στις καλές λύσεις σε αυτό το στυλό:

Λειτουργικό σετ Κατάσταση διάσωσης

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

Ενώ παίζατε με το στυλό παραπάνω, αναμφίβολα είδατε ότι το λειτουργικό setState διόρθωσε το πρόβλημά μας. Αλλά πώς ακριβώς;

Ας συμβουλευτούμε την Oprah of React - Dan.

Σημειώστε την απάντηση που έδωσε. Όταν κάνετε λειτουργικό setState…

Οι ενημερώσεις θα περιμένουν στην ουρά και θα εκτελούνται αργότερα με τη σειρά που κλήθηκαν.

So, when React encounters “multiple functional setState() calls” , instead of merging objects together, (of course there are no objects to merge) React queues the functions “in the order they were called.”

After that, React goes on updating the state by calling each functions in the “queue”, passing them the previous state — that is, the state as it was before the first functional setState() call (if it’s the first functional setState() currently executing) or the state with the latest update from the previous functional setState() call in the queue.

Again, I think seeing some code would be great. This time though, we’re gonna fake everything. Know that this is not the real thing, but is instead just here to give you an idea of what React is doing.

Also, to make it less verbose, we’ll use ES6. You can always write the ES5 version later if you want.

First, let’s create a component class. Then, inside it, we’ll create a fake setState() method. Also, our component would have a increaseScoreBy3()method, which will do a multiple functional setState. Finally, we’ll instantiate the class, just as React would do.

class User{ state = {score : 0};
 //let's fake setState setState(state, callback) { this.state = Object.assign({}, this.state, state); if (callback) callback(); }
 // multiple functional setState call increaseScoreBy3 () { this.setState( (state) => ({score : state.score + 1}) ), this.setState( (state) => ({score : state.score + 1}) ), this.setState( (state) => ({score : state.score + 1}) ) }}
const Justice = new User();

Note that setState also accepts an optional second parameter — a callback function. If it’s present React calls it after updating the state.

Now when a user triggers increaseScoreBy3(), React queues up the multiple functional setState. We won’t fake that logic here, as our focus is on what actually makes functional setState safe. But you can think of the result of that “queuing” process to be an array of functions, like this:

const updateQueue = [ (state) => ({score : state.score + 1}), (state) => ({score : state.score + 1}), (state) => ({score : state.score + 1})];

Finally, let’s fake the updating process:

// recursively update state in the orderfunction updateState(component, updateQueue) { if (updateQueue.length === 1) { return component.setState(updateQueue[0](component.state)); }
return component.setState( updateQueue[0](component.state), () => updateState( component, updateQueue.slice(1)) );}
updateState(Justice, updateQueue);

True, this is not as so sexy a code. I trust you could do better. But the key focus here is that every time React executes the functions from your functional setState, React updates your state by passing it a fresh copy of the updated state. That makes it possible for functional setState to set state based on the previous state.

Here I made a bin with the complete code. Tinker around it (possibly make it look sexier), just to get more sense of it.

FunctionalSetStateInAction

A Play with the code in this bin will be fun. Remember! we’re just faking React to get the idea...jsbin.com

Play with it to grasp it fully. When you come back we’re gonna see what makes functional setState truly golden.

The best-kept React secret

So far, we’ve deeply explored why it’s safe to do multiple functional setStates in React. But we haven’t actually fulfilled the complete definition of functional setState: “Declare state changes separately from the component classes.”

Over the years, the logic of setting-state — that is, the functions or objects we pass to setState() — have always lived inside the component classes. This is more imperative than declarative.

Well today, I present you with newly unearthed treasure — the best-kept React secret:

Thanks to Dan Abramov!

That is the power of functional setState. Declare your state update logic outside your component class. Then call it inside your component class.

// outside your component classfunction increaseScore (state, props) { return {score : state.score + 1}}
class User{ ...
// inside your component class handleIncreaseScore () { this.setState( increaseScore) }
 ...}

This is declarative! Your component class no longer cares how the state updates. It simply declares the type of update it desires.

To deeply appreciate this, think about those complex components that would usually have many state slices, updating each slice on different actions. And sometimes, each update function would require many lines of code. All of this logic would live inside your component. But not anymore!

Also, if you’re like me, I like keeping every module as short as possible, but now you feel like your module is getting too long. Now you have the power to extract all your state change logic to a different module, then import and use it in your component.

import {increaseScore} from "../stateChanges";
class User{ ...
 // inside your component class handleIncreaseScore () { this.setState( increaseScore) }
 ...}

Now you can even reuse the increaseScore function in a different component. Just import it.

What else can you do with functional setState?

Make testing easy!

You can also pass extra arguments to calculate the next state (this one blew my mind… #funfunFunction).

Expect even more in…

The Future of React

For years now, the react team has been experimenting with how to best implement stateful functions.

Functional setState seems to be just the right answer to that (probably).

Hey, Dan! Any last words?

If you’ve made it this far, you’re probably as excited as I am. Start experimenting with this functional setStatetoday!

If you feel like I’ve done any nice job, or that others deserve a chance to see this, kindly click on the green heart below to help spread a better understanding of React in our community.

If you have a question that hasn’t been answered or you don’t agree with some of the points here feel free to drop in comments here or via Twitter.

Καλή κωδικοποίηση!