26 Απρ, 2022
Νεο React API: startTransition
Τι είναι ένα Transition?
To transition είναι ένα νέο concept της React για να ξεχωρίζει τα urgent (επείγοντα) από τα non-urgent (μη-επείγοντα) updates.
- Urgent updates αντικαντοπτρίζουν άμεσα interactions όπως key presses, clicks, κλπ.
- Transition updates "μεταβαίνουν" από το ένα UI view στο άλλο.
Τα urgent updates όπως key presses, click κλπ χρειάζονται άμεση ανταπόκριση για να "ταιριάξουν" το πώς έχουμε συνηθίσει τα φυσικά αντικείμενα να συμπεριφέρονται. Ειδάλλως μοιάζουν "λάθος". Ωστόσο, τα transitions είναι διαφορετικό σενάριο διότι ο χρήστης δεν περιμένει να δει άμεσα μια τιμή στην οθόνη.
Για παράδειγμα, όταν πληκτρολογούμε σε ένα πεδίο κειμένου για να ψάξουμε κάτι, προσδοκούμε ότι το καθ'εαυτό input field θα δείξει τον χαρακτήρα που πληκτρολογήσαμε αμέσως. Ωστόσο, τα πραγματικά αποτελέσματα του search μπορούν να εμφανιστούν ξεχωριστά και μια μικρή καθυστέρηση είναι συχνά αναμενόμενη. Εάν αλλάξουμε το πεδίο κειμένου ξανά πριν τα αποτελέσματα ολοκληρώσουν την εμφάνιση τους, μας νοιάζει να δούμε μόνο τα τελευταία αποτελέσματα.
Στην React 18 μπορούμε να χρησιμοποιήσουμε το startTransition
API για να χαρακτηρίσουμε κάποια state updates ως non-urgent.
Στον παρακάτω κώδικα, υπάρχουν 2 state updates - ένα που είναι urgent και ένα που το έχουμε χαρακτηρίσει ως non-urgent καθώς το έχουμε περικλείσει μέσα σε ένα startTransition
callback:
import {startTransition} from 'react';
// Urgent: Show what was typed
setInputValue(input);
// Mark any state updates inside as transitions (non-urgent)
startTransition(() => {
// Transition: Show the results
setResults(input);
});
Τα non-urgent updates θα διακοπούν (interrupt) εάν urgent updates όπως clicks ή key presses εμφανιστούν. Εάν ένα transition γίνει interrupted από ένα χρήστη (για παράδειγμα πληκτρολογώντας πολλούς χαρακτήρες στη σειρά), η React θα "πετάξει" το μη-ολοκληρωμένο (stale) rendering work και θα κάνει render μόνο το τελευταίο update.
Demo Application: Απόδοση με και χωρίς startTransition
Φτιάξαμε μια απλή εφαρμογή για να δείξουμε την δύναμη του νέου startTransition
API.
Η εφαρμογή πρακτικά παρέχει στο χρήστη την εύρεση χαρακτήρων μέσα σε ένα μεγάλο κείμενο - στην περίπτωση μας περίπου 60000 χαρακτήρες.
Η εφαρμογή μας πρέπει να προσφέρει τα δυο παρακάτω features:
- Να δείχνει ένα input που θα επιτρέπει στο χρήστη να πληκτρολογήσει ένα όρο αναζήτησης.
- Να υπογραμμίζει τους χαρακτήρες που ταιριάζουν (ανεξαρτήτως μικρά ή κεφαλαία) με τον όρο αναζήτησης.
Ο απλός αλγόριθμος που χρησιμοποιήσαμε είναι:
- Δημιουργία ενός regex με βάση τον όρο αναζήτησης που θα ταιριάξει χαρακτήρες (case insensitive)
- Προσθήκη της κλάσης
highlight
σε όλα τα spans των χαρακτήρων που ταιριάζουν.
Ο κώδικας για τη συνάρτηση applyFilter
φαίνεται παρακάτω:
const applyFilter = (query, setFilteredNode) => {
setFilteredNode(() => {
if (!query || query.trim().length === 0) {
return INITIAL_VALUE;
}
const regex = new RegExp(
query.trim().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"),
"ig"
);
const node = TEXT.replaceAll(regex, (s) => `##${s}##`)
.split("##")
.map((s, i) => (
<span key={i} className={i % 2 === 1 ? "highlight" : ""}>
{s}
</span>
));
return node;
});
};
Προφανώς θα μπορούσαμε να σκεφτούμε πιο αποδοτικούς αλγορίθμους, και αυτό το regex σίγουρα επηρρεάζει πολύ την απόδοση. Ωστόσο ο στόχος μας εδώ είναι να "δημιουργήσουμε" μια κοστοβόρα συνάρτηση που προκαλεί ένα heavy React state update ώστε να δούμε ένα το startTransition
API θα βελτιώσει την κατάσταση.
Μπορείτε να βρείτε όλο τον κώδικα εδώ.
1η Υλοποίηση: Unoptimized χωρίς startTransition
Η πρώτη μας υλοποίηση είναι ένα component με δυο απλά state updates, ένα για το input και ένα που κάνει apply τα φιλτραρισμένα αποτελέσματα. Όποτε αλλάζει το input value, η συνάρτηση applyFilter
καλείται. Και τα δυο state updates χαρακτηρίζονται ως urgent by default:
import {useState} from 'react';
const Unoptimized = () => {
const [query, setQuery] = useState("");
const [filteredNode, setFilteredNode] = useState(INITIAL_VALUE);
const onInputChange = (e) => setQuery(e.target.value);
useEffect(() => {
applyFilter(query, setFilteredNode);
}, [query]);
return (
<>
<div className="input">
<input onChange={onInputChange} />
</div>
<div className="text">{filteredNode}</div>
</>
);
};
Unoptimized
Μπορείτε να δείτε ότι οι χαρακτήρες δεν εμφανίζονται αμέσως μέσα στο input και το UI φαίνεται κάπως "αργό":
Unoptimized με CPU 4x slowdown
Εφαρμόσαμε 4x CPU slowdown (από το Chrome dev tools) για να προσωμοιώσουμε καλύτερα ένα average CPU ή κινητή συσκευή. Μπορούμε να δούμε ότι το UI σίγουρα φαίνεται αργό και καθόλου smooth:
2η Υλοποίηση: Optimized με startTransition
Στην 2η υλοποίηση μας χρησιμοποιούμε το startTransition
API. Συγκεκριμένα χρησιμοποιούμε το useTransition
hook που επιστρέφει και μια pending
μεταβλητή την οποία εκμεταλλευόμαστε για να δείξουμε στο χρήστη ότι ένα transition είναι ακόμη σε επεξεργασία θολώνοντας την οθόνη (θα μπορούσαμε να δείξουμε και κάποιο loader).
Χαρακτηρίζουμε το state update των αποτελεσμάτων ως non-urgent περικλείοντάς το μέσα σε startTransition
callback:
import {useState, useTransition} from 'react';
const Optimized = () => {
const [query, setQuery] = useState("");
const [filteredNode, setFilteredNode] = useState(INITIAL_VALUE);
const [pending, startTransition] = useTransition();
const onInputChange = (e) => setQuery(e.target.value);
useEffect(() => {
startTransition(() => {
applyFilter(query, setFilteredNode);
});
}, [query, startTransition]);
return (
<>
{pending && <div className="fade" />}
<div className="input">
<input onChange={onInputChange} />
</div>
<div className="text">{filteredNode}</div>
</>
);
};
Optimized
Δείτε πόσο γρήγορα οι χαρακτήρες εμφανίζονται στο input - φαίνεται φυσικό, smooth και αυτό που θα περίμενε ο χρήστης. Θολώνουμε την οθόνη όσο η μεταβλητή pending
είναι true ώστε να δείξουμε στο χρήστη ότι το transition είναι ακόμη σε επεξεργασία.
Optimized με CPU 4x slowdown
Εφαρμόσαμε ακόμη και CPU 4x slowdown για να εξετάσουμε πώς αποκρίνεται το UI. Φαίνεται καλό, ακόμη και σε αυτό το σενάριο!
Εναλλακτικές λύσεις πριν την React 18?
Πριν την React 18 δεν είχαμε το startTransition
API, όποτε πώς διορθώναμε τέτοιες περιστάσεις σε προηγούμενες React versions?
Η πιο συχνή λύση για να αντιμετωπίσουμε τέτοια προβλήματα και να αποφύγουμε βαριά απανωτά state updates ήταν να χρησιμοποιήσουμε debounce ή throttle τεχνικές.
Για παράδειγμα θα μπορούσαμε να χρησιμοποιήσουμε αυτή την εκπληκτική use-debounce βιβλιοθήκη και να περικλείσουμε τα βαριά state updates των αποτελεσμάτων μέσα σε ένα debounced callback κάπως έτσι:
import {useState} from 'react';
import {useDebouncedCallback} from 'use-debounce';
const Debounced = () => {
const [query, setQuery] = useState("");
const [filteredNode, setFilteredNode] = useState(INITIAL_VALUE);
const onInputChange = (e) => setQuery(e.target.value);
const debounced = useDebouncedCallback(
(query, setFilteredNode) => {
applyFilter(value, setFilteredNode);
},
100 // debounce 100 ms
);
useEffect(() => {
debounced(query, setFilteredNode);
}, [query, startTransition]);
return (
<>
{pending && <div className="loader" />}
<div className="input">
<input onChange={onInputChange} />
</div>
<div className="text">{filteredNode}</div>
</>
);
};
Αλλά υπάρχουν ακόμη κάποια προβλήματα με αυτή τη προσέγγιση.
Πρώτον, η συνάρτηση applyFilter
δεν μπορεί να καλεσθεί σε κάτω από 100ms (ή οσοδήποτε χρόνο επιλέξουμε) από τη στιγμή που ο χρήστης σταματήσει να πληκτρολογεί. Από την άλλη μεριά, χρησιμοποιώντας το startTransition
API, η βαριά επεξεργασία των αποτελέσματων ξεκινάει το συντομότερο δυνατό, χωρίς να χρειάζεται να περιμένει κάποιο χρόνο.
Δεύτερον, ακόμη και αν χρησιμοποιούσαμε throttle αντί για debounce για να προσπαθήσουμε να λύσουμε το παραπάνω, η επεξεργασία των αποτελεσμάτων είναι un-interruptable. Αυτό σημαίνει ότι εάν έρθουν urgent updates (όπως key pressing) όσο τα αποτελέσματα είναι ακόμη σε επεξεργασία, τo UI αναπόφευκτα θα είναι unresponsive. Ενώ, με το startTransition
API, η επεξεργασία των αποτελεσμάτων θα γίνει interrupted όταν έρθουν τα urgent updates, κρατώντας έτσι το input field responsive.
Τέλος
Ελπίζω να βρήκατε το άρθρο χρήσιμο.
Τα λέμε σύντομα! 🙂
Εγγραφή
Εγγραφειτε στην λιστα
Εγγραφείτε με το e-mail σας για να σας στέλνω το υλικό μου. Δεν θα είναι spam, σας το υπόσχομαι! Μπορείτε να καταργήσετε την εγγραφή σας όποτε θέλετε.