17 Δεκ, 2021
React 18: Automatic Batching
Είμαι σίγουρος ότι έχετε ακούσει τον όρο "React Batching" στο παρελθόν, και ίσως να είχατε μπερδευτεί λιγάκι (όπως είχα εγώ) για το τι ακριβώς είναι και τι κάνει.
Γενικά μιλώντας η λέξη Batching μπορεί να χρησιμοποιηθεί με διάφορους τρόπους αλλά ένας γενικός ορισμός που έχει νόημα στην περίπτωση μας μπορεί να εκφραστεί ως:
Μια παρτίδα από δουλειές, δεδομένα ή προγράμματα τα οποία θεωρούνται ως μια μονάδα για υπολογιστική επεξεργασία.
(Batch Processing) Μια μορφή επεξεργασίας δεδομένων στην οποία είσοδοι ομαδοποιούνται για επεξεργασία στον ίδιο κύκλο εκτέλεσης.
Τι είναι το React Batching
Όταν, η React εφαρμόζει "Batching" σημαίνει ότι "ομαδοποιεί πολλαπλά state updates και τα εκτελεί με ένα re-render" κυρίως για καλύτερη απόδοση.
Στην React 17 και προηγούμενες εκδόσεις της 17, η React θα κάνει batch αυτόματα οποιαδήποτε state updates συμβούν μόνο μέσα σε React event handlers (όπως ένα click ή change).
Ρίξτε μια ματιά στον κώδικα παρακάτω και μαντέψτε πόσα re-renders θα προκαλέσει η handleClick
συνάρτηση;
Επίσης μην ανησυχείτε για την useRenderCount
, είναι απλά ένα hook για να μετράμε τα re-renders :-).
const useRenderCount = () => {
const renderCounter = useRef(0);
useLayoutEffect(() => {
renderCounter.current++;
});
return renderCounter.current;
};
Το actual demo:
const Demo1 = () => {
const renderCount = useRenderCount(); // we use this to count re-renders
const [countUp, setCountUp] = useState(0);
const [countDown, setCountDown] = useState(0);
const handleClick = () => {
setCountUp((cUp) => cUp + 1);
setCountDown((cDown) => cDown - 1);
};
return (
<div style={{ textAlign: "center" }}>
<h4>Demo 1 (Batching)</h4>
<button onClick={handleClick}>Click me</button>
<h2>Count up: {countUp}</h2>
<h2>Count down: {countDown}</h2>
<div>Number of rerenders: {renderCount}</div>
</div>
);
}
Ακριβώς, θα προκαλέσει μόνο ένα re-render επειδή η React θα κάνει batch τα δυο state updates setCountUp
και setCountDown
σε ένα!
Αρκετά κουλ έτσι; Η React θα το τακτοποιήσει αυτό για εμάς αυτόματα και θα αποφύγει περιττά re-renders το οποίο είναι τέλειο για την απόδοση! Επίσης θα αποτρέψει τα components μας από το να κάνουν render "μισο-τελειωμένα" states όπου μόνο μια state μεταβλητή έχει ανανεωθεί, το οποίο μπορεί να οδηγήσει σε bugs.
✋ Όμως προσοχή...
Η React (πριν την έκδοση 18) θα εφαρμόσει batch μόνο μέσα σε React event handlers. Δεν θα εφαρμόσει batch σε updates μέσα σε promises, setTimeout, native event handlers ή άλλα events.
Στον πραγματικό κόσμο υπάρχουν αρκετές περιπτώσεις όπου χρειάζεται να κάνουμε πολλαπλά state updates μεσα σε ένα handler όπου η React δεν μπορεί να κάνει batch.
Ας δούμε μερικά παραδείγματα όπου η React δεν μπορεί να κάνει batch τα updates και θα προκαλέσει παραπάνω από ένα re-render.
1) Updates μέσα σε setTimeout
Στο demo παρακάτω, τα updates setCountUp
και setCountDown
θα προκαλέσουν δύο re-renders καθώς η React δε μπορεί να κάνει batch updates που βρίσκονται μέσα σε setTimeout
callback.
const Demo2 = () => {
const renderCount = useRenderCount();
const [countUp, setCountUp] = useState(0);
const [countDown, setCountDown] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCountUp((cUp) => cUp + 1);
setCountDown((cDown) => cDown - 1);
}, 500);
};
return (
<div className="Demo">
<h4>Demo 2 (setTimeout)</h4>
<button onClick={handleClick}>Click me</button>
<h2>Count up: {countUp}</h2>
<h2>Count down: {countDown}</h2>
<div>Number of rerenders: {renderCount}</div>
</div>
);
};
2) Updates inside a promise
Στο παρακάτω παράδειγμα κάνουμε fetch μια τυχαία εικόνα γάτας όταν κλικάρουμε το κουμπί. Η React δε μπορεί να κάνει batch τα πολλαπλά state updates που βρίσκονται μετά το await
fetch call, συγκεκριμένα το setCat(json.url)
και setLoading(false)
.
Ωστόσο, μπορεί επιτυχώς να κάνει batch τις πρώτες δυο κλήσεις πριν το await: setLoading(true)
and setCat(null)
.
Οπότε συνολικά η συνάρτηση handleClick
θα προκαλέσει 3 re-renders.
const Demo3 = () => {
const renderCount = useRenderCount();
const [loading, setLoading] = useState(false);
const [cat, setCat] = useState(null);
const handleClick = async () => {
// these two below will be batched
setLoading(true);
setCat(null);
const response = await fetch("https://thatcopy.pw/catapi/rest/");
if (response.ok) {
const json = await response.json();
setCat(json.url); // cannot batch
}
setLoading(false); // cannot batch
};
return (
<div className="Demo">
<h4>Demo 3 (Promise)</h4>
<button onClick={handleClick}>Click me</button>
<h2>Loading: {String(loading)}</h2>
<h2
style={{
display: "flex",
justifyContent: "center",
alignItems: "center"
}}
>
Cat:{" "}
{cat ? (
<img
alt="cat"
width="35"
height="35"
src={cat}
style={{ marginLeft: "8px" }}
/>
) : (
""
)}
</h2>
<div>Number of rerenders: {renderCount}</div>
</div>
);
};
3) Updates inside a native event handler
Στο παράδειγμα παρακάτω χρησιμοποιούμε ένα native event handler κάνοντας attach ένα click
listener στο κουμπί μας. Η React δεν μπορεί να κάνει batch τα updates αυτά επίσης.
const Demo4 = () => {
const renderCount = useRenderCount();
const [countUp, setCountUp] = useState(0);
const [countDown, setCountDown] = useState(0);
useEffect(() => {
const handleClick = () => {
setCountUp((cUp) => cUp + 1);
setCountDown((cDown) => cDown - 1);
};
const element = document.querySelector("#my-button");
element.addEventListener("click", handleClick);
return () => {
element.removeEventListener('click', handleClick);
};
}, []);
return (
<div className="Demo">
<h4>Demo 4 (Native event handler)</h4>
<button id="my-button">Click me</button>
<h2>Count up: {countUp}</h2>
<h2>Count down: {countDown}</h2>
<div>Number of rerenders: {renderCount}</div>
</div>
);
};
💡 Γρήγορο fix για τα παραπάνω σενάρια
Προφανώς, μια γρήγορη λύση θα ήταν να αποθηκεύσουμε όλα τα "επιμέρους" states σε ένα μεγάλο state (ή να χρησιμοποιήσουμε useReducer).
Για παράδειγμα, το Demo με το fetch promise θα μπορούσε να γραφτεί όπως παρακάτω ώστε να αποφύγουμε ανεπιθύμητα re-renders:
const Demo3Fix = () => {
const renderCount = useRenderCount();
const [state, setState] = useState({
loading: false,
cat: null
}); // a single state object
const handleClick = async () => {
setState({
loading: true,
cat: null
}); // 1st re-render
const response = await fetch("https://thatcopy.pw/catapi/rest/");
if (response.ok) {
const json = await response.json();
setState((s) => ({ cat: json.url, loading: false })); // 2nd re-render
} else {
setState((s) => ({ ...s, loading: false })); // or this 2nd re-render
}
};
const { cat, loading } = state;
return (
...
);
};
➡️ Όλα τα προηγούμενα React 17 demos είναι διαθέσιμα σε αυτό το CodeSandbox: https://codesandbox.io/s/cranky-mccarthy-g5ntm
React 18 είναι εδώ 🎉
Στην version 18 της React (όπου είναι ακόμη σε beta), γίνεται αυτόματο batching για όλα τα use cases ώστε να βελτιωθεί η απόδοση ακόμη περισσότερο. Μπορείτε να διαβάσετε περισσότερα εδώ.
Αυτό σημαίνει ότι σε όλα μας τα προηγούμενα μας παραδείγματα, η React (18) θα κάνει αυτόματα batch όλα τα πολλαπλά state updates σε ένα update.
Για του λόγου το αληθές, ας δούμε το setTimeout demo όπου το μόνο πράγμα αλλάξαμε ήταν η version της React σε 18 και ο κώδικας μέσα στο αρχείο index.js
όπου λέμε στην React πως να φορτώσει την εφαρμογή μας. Συγκεκριμένα, για να χρησιμοποιήσει η React 18 αυτά τα νέα features, πρέπει να χρησιμοποιήσουμε το createRoot
api.
index.js
στην React 17:
import { render } from "react-dom";
import App from "./App";
render(<App />, document.getElementById("root"));
index.js
στην React 18:
import { createRoot } from "react-dom";
import App from "./App";
createRoot(document.getElementById("root")).render(<App />);
➡️ Εδώ είναι ο κώδικας με όλα τα demos σε React 18: https://codesandbox.io/s/objective-elbakyan-6u76m
Εάν δεν θέλουμε να κάνουμε batch;
Ίσως υπάρχει κάποιο σενάριο όπου δε θέλουμε η React να κάνει batch updates (αν και το batching γενικά θεωρείται ασφαλές). Μπορούμε να το πετύχουμε αυτό χρησιμοποιώντας τη συνάρτηση flushSync
ως εξής:
import { flushSync } from 'react-dom';
const handleClick = () => {
flushSync(() => {
setCountUp((cUp) => cUp + 1); // 1st re-render
})
flushSync(() => {
setCountDown((cDown) => cDown - 1); // 2nd re-render
})
};
Επίσης, αξίζει να αναφέρουμε ότι σε εκδόσεις της React πριν την 18, μπορούσαμε να αναγκάσουμε (force) state updates έξω απο React event handlers να γίνουν batch χρησιμοποιώντας το undocumented API unstable_batchedUpdates
έτσι:
unstable_batchedUpdates(() => {
// these 2 below will be batched
setCountUp((cUp) => cUp + 1);
setCountDown((cDown) => cDown - 1);
});
Σύμφωνα με τα docs αυτό το API ακόμη υπάρχει στην React 18, αλλά δεν είναι απαραίτητο πια επειδή το batching εφαρμόζεται αυτόματα. Επίσης ίσως αφαιρεθεί σε μελλοντικές εκδόσεις.
Τέλος
Ελπίζω να σας άρεσε αυτό το άρθρο, τα λέμε μετά! 😃
Εγγραφή
Εγγραφειτε στην λιστα
Εγγραφείτε με το e-mail σας για να σας στέλνω το υλικό μου. Δεν θα είναι spam, σας το υπόσχομαι! Μπορείτε να καταργήσετε την εγγραφή σας όποτε θέλετε.