Le guide des nouveaux crochets dans React 18
Dans cet article de blog, nous ne décrirons pas seulement ce que font les crochets React, mais nous mettrons également la main à la pâte et expliquerons comment les utiliser dans le code.
React 18 est une version majeure qui comprend de nouvelles fonctionnalités, telles que le rendu simultané, le traitement par lots automatisé, les transitions, de nouvelles API et des hooks. Dans ce didacticiel, nous aborderons les cinq nouveaux crochets qui sont arrivés dans Réagir 18:
- ID d’utilisation
- useDeferredValue
- utiliserTransition
- useSyncExternalStore
- useInsertionEffect
Vous vous sentez un peu rouillé sur le sujet des crochets en général ? Ensuite, vous pouvez consulter le Guide d’apprentissage des crochets React nos experts React ont préparé. Et si vous souhaitez en savoir plus sur les crochets les plus couramment utilisés, consultez ce blog sur useCallback et useRef.
Cependant, au lieu de simplement décrire ce qu’ils font et comment les utiliser, nous allons nous salir les mains et expliquer comment les utiliser réellement dans le code. Pour cela, nous allons créer des solutions artificielles de gestion d’état et CSS-in-JS. Plongeons-nous !
Code de projet React Hooks
Vous pouvez trouver des exemples de code complets pour ce projet dans cette Référentiel GitHub. Ci-dessous, vous pouvez également trouver un StackBlitz interactif.
Configuration du projet React Hooks
Si vous souhaitez suivre ce tutoriel, vous pouvez rapidement échafauder un nouveau projet React en utilisant Vite en exécutant la commande ci-dessous :
$ npm create vite@latest react-18-hooks --template react
Une fois le projet échafaudé, déplacez-vous dedans, installez toutes les bibliothèques et démarrez le serveur de développement.
$ cd react-18-hooks && npm install && npm run dev
Nous utiliserons Vent arrière pour les styles, mais au lieu de passer par tout le processus de configuration, nous tirerons parti de la version CDN. Il suffit de mettre à jour le
index.html
fichier et ajoutez le script ci-dessous.
index.html
<script src="https://cdn.tailwindcss.com"></script>
Voilà pour la configuration. Jetons un coup d’œil au premier nouveau crochet—useId
.
Si vous n’avez jamais entendu parler de Vite auparavant, j’ai écrit un article à ce sujet—Qu’est-ce que Vite : le guide des outils de projet modernes et ultra-rapides.
ID d’utilisation
L’époque où React ne fonctionnait que côté client est révolue depuis longtemps. Avec des frameworks comme Next.js, Gatsby ou Remixer et de nouvelles fonctionnalités telles que les composants serveur, React est utilisé à la fois côté client et côté serveur.
Jusqu’à la version 18, React n’offrait aucun bon moyen de générer des identifiants uniques pouvant être utilisés pour le contenu rendu par le serveur et le client. La chose importante à retenir est que le balisage HTML rendu sur le client doit correspondre à celui rendu sur le serveur. Sinon, vous serez accueilli avec une erreur de non-concordance d’hydratation du serveur React.
Voici une situation dans laquelle cela pourrait se produire : Disons que nous avons un formulaire avec un champ de saisie pour lequel nous devons générer un identifiant unique.
const Comp = props => {
const uid = uuid()
return (
<form>
<label htmlFor={uid}>Name</label>
<input id={uid} type="text" />
</form>
)
}
Le composant ci-dessus aurait des identifiants générés une fois sur le serveur, et de nouveaux seraient générés côté client. Cela entraînerait une incompatibilité dans le DOM. C’est là que le useId
crochet entre en jeu.
import { useId } from 'react'
const Comp = props => {
const uid = useId()
return (
<form>
<label htmlFor={uid}>Name</label>
<input id={uid} type="text" />
</form>
)
}
La useId
hook peut être utilisé pour générer des identifiants uniques qui seront les mêmes côté serveur et côté client et ainsi aider à éviter l’erreur de non-concordance.
Si nous avons plus d’un champ, nous pouvons toujours utiliser l’interpolation de chaîne et ajouter un préfixe ou un suffixe à l’identifiant unique. Vous pouvez créer un nouveau fichier appelé UseIdExample.jsx
avec le code ci-dessous.
src/exemples/UseIdExample.jsx
import { useId } from "react";
const UseIdExample = props => {
const uid = useId();
return (
<div>
<labelhtmlFor={`${uid}-name`}>
Name
</label>
<input
id={`${uid}-name`}
/>
<div>
Generated unique user input id: {`${uid}-name`}
</div>
<label htmlFor={`${uid}-age`}>
Age
</label>
<input
id={`${uid}-age`}
/>
<div>Generated unique age input id: {`${uid}-age`}</div>
</div>
);
};
export default UseIdExample;
Ensuite, mettez à jour le App
composant pour rendre le UseIdExample
.
src/App.jsx
import "./App.css";
import UseIdExample from "./examples/UseIdExample";
function App() {
return (
<div className="App space-y-16">
<UseIdExample />
</div>
);
}
export default App;
L’image ci-dessous montre ce que vous devriez voir. React génère un identifiant unique avec deux-points comme préfixe et suffixe.
useDeferredValue et useTransition
Avec le nouveau moteur de rendu simultané, React peut interrompre et mettre en pause les rendus. Cela signifie que si un nouveau rendu haute priorité est programmé, React peut arrêter le processus de rendu basse priorité actuel et gérer le prochain en premier.
Un rendu de haute priorité peut être causé par un clic ou une saisie d’un utilisateur. React propose deux nouveaux Crochets de réaction qui peut être utilisé pour indiquer les mises à jour de faible priorité—
useDefferedValue
et useTransition
. Cela offre une nouvelle façon d’optimiser les applications React, car les développeurs peuvent désormais spécifier quelles mises à jour d’état sont de faible priorité.
useDeferredValue
Voyons d’abord le useDeferredValue
accrocher. Ci-dessous, nous avons une fonctionnalité simple qui permet à un utilisateur de rechercher des repas.
src/exemples/UseDeferredValueExample.jsx
import {
memo,
Suspense,
useDeferredValue,
useEffect,
useRef,
useState,
} from "react";
const Meals = memo(props => {
const { query } = props;
const abortControllerRef = useRef(null);
const [meals, setMeals] = useState([]);
const searchMeals = async query => {
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
const response = await fetch(
`https://www.themealdb.com/api/json/v1/1/search.php?s=${query}`,
{
signal: abortControllerRef.current.signal,
}
);
const data = await response.json();
setMeals(data.meals || []);
};
useEffect(() => {
searchMeals(query);
return () => {
abortControllerRef.current?.abort();
}
}, [query]);
return (
<>
{Array.isArray(meals) ? (
<ul className="mt-3 space-y-2 max-h-[30rem] overflow-auto">
{meals.map(meal => {
const { idMeal, strMeal } = meal;
return <li key={idMeal}>{strMeal}</li>;
})}
</ul>
) : null}
</>
);
});
const UseDeferredValueExample = props => {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
return (
<div>
<h2 className="text-xl font-bold mb-4">useDeferredValue Example</h2>
<div>
<div>
<label htmlFor="mealQuery" className="mb-1 block">
Meal
</label>
</div>
<input
id="mealQuery"
className="shadow border border-slate-100 px-4 py-2"
value={query}
onChange={e => {
setQuery(e.target.value);
}}
/>
</div>
<Suspense fallback="Loading results...">
<Meals query={deferredQuery} />
</Suspense>
</div>
);
};
export default UseDeferredValueExample;
Lorsqu’un utilisateur saisit quelque chose dans l’entrée de la requête, le query
l’état est mis à jour. Cependant, au lieu de le transmettre directement au Meals
composant, il est passé au useDeferredValue
crochet à la place. Le crochet renvoie un
deferredQuery
valeur, qui est ensuite transmise au Meals
composant. Nous laissons React décider quand exactement le deferredQuery
changement d’état au plus tard query
évaluer.
Notez que le Meals
composant est enveloppé avec memo
pour s’assurer qu’il ne se restitue que lorsque deferredQuery
changements et non query
. La useDeferredValue
hook est similaire au fonctionnement du rebond ou de la limitation, mais la différence est qu’au lieu d’attendre qu’un laps de temps spécifié se soit écoulé, React peut démarrer le travail immédiatement lorsqu’il est terminé avec un travail de priorité plus élevée.
Maintenant, nous devons ajouter le UseDeferredValueExample
composant dans le App.jsx
dossier.
src/App.jsx
import "./App.css";
import UseIdExample from "./examples/UseIdExample";
import UseDeferredValueExample from "./examples/UseDeferredValueExample";
function App() {
return (
<div className="App space-y-16">
<UseIdExample />
<UseDeferredValueExample />
</div>
);
}
export default App;
Le GIF ci-dessous montre à quoi devrait ressembler la fonctionnalité de recherche.
Ensuite, regardons le useTransition
accrocher.
utiliserTransition
La useTransition
crochet est assez similaire à useDeferredValue
, mais nous avons plus de contrôle sur le démarrage d’une mise à jour de faible priorité. La useTransition
hook renvoie un tuple avec le isPending
valeur qui indique si une transition est en cours et la startTransition
méthode.
const [isPending, startTransition] = useTransition()
Remplaçons le useDeferredValue
crochet de notre exemple précédent et utilisez le useTransition
crochet à la place.
src/exemples/UseTransitionExample.jsx
import {
memo,
Suspense,
useTransition,
useEffect,
useRef,
useState,
} from "react";
const Meals = memo(props => {
const { query } = props;
const [meals, setMeals] = useState([]);
const abortControllerRef = useRef(null);
const [isPending, startTransition] = useTransition();
const searchMeals = async query => {
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
const response = await fetch(
`https://www.themealdb.com/api/json/v1/1/search.php?s=${query}`,
{
signal: abortControllerRef.current.signal,
}
);
const data = await response.json();
startTransition(() => {
setMeals(data.meals || []);
});
};
useEffect(() => {
searchMeals(query);
return () => {
abortControllerRef.current?.abort();
};
}, [query]);
return (
<>
{isPending ? <p>Loading...</p> : null}
{Array.isArray(meals) ? (
<ul className="mt-3 space-y-2 max-h-[30rem] overflow-auto">
{meals.map(meal => {
const { idMeal, strMeal } = meal;
return <li key={idMeal}>{strMeal}</li>;
})}
</ul>
) : null}
</>
);
});
const UseTransitionExample = props => {
const [query, setQuery] = useState("");
return (
<div>
<h2 className="text-xl font-bold mb-4">useTransition Example</h2>
<div>
<div>
<label htmlFor="mealQuery" className="mb-1 block">
Meal
</label>
</div>
<input
id="mealQuery"
className="shadow border border-slate-100 px-4 py-2"
value={query}
onChange={e => {
setQuery(e.target.value);
}}
/>
</div>
<Suspense fallback="Loading results...">
<Meals query={query} />
</Suspense>
</div>
);
};
export default UseTransitionExample;
Au lieu d’avoir un état différé pour le query
valeur, nous enveloppons le setMeals
mettre à jour avec le startTransition
Au lieu.
startTransition(() => {
setMeals(data.meals || []);
});
Si React était en train de traiter le setMeals
mise à jour mais une mise à jour de priorité plus élevée, comme un clic d’utilisateur, serait planifiée, le setMeals
la mise à jour serait interrompue.
Enfin, rendez UseTransitionExample
dans le App
composant.
src/App.jsx
import "./App.css";
import UseIdExample from "./examples/UseIdExample";
import UseDeferredValueExample from "./examples/UseDeferredValueExample";
import UseTransitionExample from "./examples/useTransitionExample";
function App() {
return (
<div className="App space-y-16">
<UseIdExample />
<UseDeferredValueExample />
<UseTransitionExample />
</div>
);
}
export default App;
useSyncExternalStore
La useSyncExternalStore
est un crochet qui a été créé pour les bibliothèques de gestion d’état. Son objectif est de fournir la possibilité de lire et de s’abonner à partir de sources de données externes d’une manière qui fonctionne avec des fonctionnalités de rendu simultanées telles que l’hydratation sélective et le découpage temporel.
Un magasin externe doit fournir au moins deux arguments : les méthodes d’abonnement et d’obtention d’instantané d’état. Le premier permet à React de s’abonner à toutes les modifications du magasin, et le second renvoie l’état du magasin. Voici un exemple simple d’utilisation de useSyncExternalStore
accrocher.
const state = useSyncExternalStore(store.subscribe, store.getState);
La store.getState
renvoie l’intégralité de l’état externe, mais nous pouvons également passer une fonction qui n’en renvoie qu’une partie. Par exemple, si le magasin a un champ appelé name
nous pourrions obtenir juste le name
valeur de l’état du magasin.
const state = useSyncExternalStore(
store.subscribe,
() => store.getState().name
);
La useSyncExternalStore
peut également accepter un troisième argument, qui peut être utilisé pour fournir un instantané d’état qui a été créé si l’application React était rendue côté serveur. Nous n’allons pas nous y plonger dans cet article, car le rendu côté serveur est livré avec ses propres règles et sa propre configuration qui sortent du cadre de cet article.
const state = useSyncExternalStore(
store.subscribe,
store.getState,
() => INITIAL_SERVER_SNAPSHOT
);
C’est une bonne explication jusqu’à présent, mais comment pourrions-nous l’utiliser dans la pratique ? Heureusement, la création d’une bibliothèque de gestion d’état ne doit pas nécessairement être extrêmement compliquée, et Condition en est un bon exemple. Voici une mini implémentation de Zustand utilisant le useSyncExternalStore
accrocher.
Tout d’abord, nous avons besoin d’une logique de création de magasin.
src/exemples/createStore.js
import { useSyncExternalStore } from "react";
import produce from "immer";
export const createStore = createStateFn => {
let state = {};
const listeners = new Set();
const subscribe = listener => {
listeners.add(listener);
return () => listeners.delete(listener);
};
const setState = updater => {
const prevState = state;
const nextState = produce(state, updater);
state = nextState;
listeners.forEach(listener => listener(nextState, prevState));
};
const getState = () => state;
const useStore = selector => {
return useSyncExternalStore(
subscribe,
typeof selector === "function" ? () => selector(getState()) : getState
);
};
useStore.subscribe = subscribe;
state = createStateFn(setState, getState);
return useStore;
};
La createStore
crée un nouvel état et quelques méthodes :
subscribe
– ajoute un nouvel écouteur qui sera notifié lorsque l’état changesetState
– met à jour l’état de manière immuable en utilisant immer et notifie tous les abonnésgetState
– renvoie l’état actueluseStore
– un emballage autouruseSyncExternalStore
qui peut être utilisé pour consommer l’état du magasin
Maintenant, nous pouvons utiliser le createStore
méthode pour créer un nouveau magasin. Ci-dessous, nous créons un magasin de comptage avec des méthodes pour incrémenter, décrémenter, diviser et multiplier le comptage.
src/exemples/UseSyncExternalStoreExample.jsx
import { useEffect } from "react";
import { createStore } from "./createStore";
const useCountStore = createStore(set => {
return {
count: 0,
decrement: () => {
set(state => {
state.count -= 1;
});
},
increment: () => {
set(state => {
state.count += 1;
});
},
divide: () => {
set(state => {
state.count /= 2;
});
},
multiply: () => {
set(state => {
state.count *= 2;
});
},
};
});
const UseSyncExternalStoreExample = props => {
const countStore = useCountStore();
const multipliedCount = useCountStore(store => store.count * 2);
const multiply = useCountStore(store => store.multiply);
useEffect(() => {
const unsubscribe = useCountStore.subscribe((state, prevState) => {
console.log("State changed");
console.log("Prev state", prevState);
console.log("New state", state);
});
return unsubscribe;
}, []);
return (
<div>
<h2 className="text-xl font-bold mb-4">useSyncExternalStore Example</h2>
<div>Count: {countStore.count}</div>
<div>Multiplied Count: {multipliedCount}</div>
<div className="flex gap-4 mt-4">
<button
className="bg-sky-700 text-sky-100 px-4 py-3"
onClick={countStore.decrement}
>
Decrement
</button>
<button
className="bg-sky-700 text-sky-100 px-4 py-3"
onClick={countStore.increment}
>
Increment
</button>
<button
className="bg-sky-700 text-sky-100 px-4 py-3"
onClick={countStore.divide}
>
Divide
</button>
<button
className="bg-sky-700 text-sky-100 px-4 py-3"
onClick={multiply}
>
Multiply
</button>
</div>
</div>
);
};
export default UseSyncExternalStoreExample;
Enfin, ajoutez le UseSyncExternalStoreExample
composant dans le App.jsx
dossier.
src/App.jsx
import "./App.css";
import UseSyncExternalStoreExample from "./examples/UseSyncExternalStoreExample";
function App() {
return (
<div className="App space-y-16">
<UseSyncExternalStoreExample />
</div>
);
}
export default App;
Voici à quoi ressemble notre implémentation en action.
Notez que le code de création de magasin n’est pas vraiment optimisé, donc si vous aimez cette approche d’utilisation d’un état externe, utilisez simplement la bibliothèque Zustand.
useInsertionEffect
La useInsertionEffect
ne doit être utilisé que par les bibliothèques CSS-in-JS pour insérer dynamiquement des styles dans le DOM. Ce crochet a une signature identique à useEffect
, mais il s’exécute de manière synchrone avant toutes les mutations DOM. Ainsi, si vous n’injectez aucun style CSS dans le DOM, vous ne devriez pas l’utiliser.
Je me suis demandé si je devais créer un exemple pratique de la façon d’utiliser le useInsertionEffect
puisque je n’ai jamais vraiment regardé sous le capot des bibliothèques CSS-in-JS, mais la voici. Une implémentation CSS-in-JS naïve, artificielle et totalement non optimisée, ce qui signifie qu’il ne faut pas l’utiliser à la maison.
Premièrement, nous avons le useStyles
crochet qui accepte un objet avec des styles et des accessoires.
src/exemples/useStyles.js
import { useInsertionEffect, useMemo } from "react";
import { nanoid } from "nanoid";
import styleToCss from "style-object-to-css-string";
export const useStyles = (stylesCreator, props = {}) => {
const [styles, styleRules] = useMemo(() => {
const styles = {};
const styleRules = [];
for (const [styleProperty, styleValue] of Object.entries(
stylesCreator(props)
)) {
const hashedClassName = `${styleProperty}_${nanoid()}`;
styles[styleProperty] = hashedClassName;
const rule = `.${hashedClassName} {${styleToCss(styleValue)}}`;
styleRules.push(rule);
}
return [styles, styleRules];
}, [stylesCreator, props]);
useInsertionEffect(() => {
const stylesheet = document.createElement("style");
document.head.appendChild(stylesheet);
for (const rule of styleRules) {
stylesheet.sheet.insertRule(rule);
}
return () => {
document.head.removeChild(stylesheet);
};
}, [styles, styleRules]);
return styles;
};
Dans le useMemo
un nom de classe haché unique est généré pour chaque propriété de style d’objet, et l’objet avec styles est converti en une chaîne CSS à l’aide de la propriété style-object-to-css-string
bibliothèque. Chaque règle est poussée dans le
styleRules
déployer.
const hashedClassName = `${styleProperty}_${nanoid()}`;
styles[styleProperty] = hashedClassName;
const rule = `.${hashedClassName} {${styleToCss(styleValue)}}`;
styleRules.push(rule);
Dans le useInsertionEffect
nous créons un nouveau style
élément, parcourez les règles de style et insérez chacune d’elles dans la feuille de style.
const stylesheet = document.createElement("style");
document.head.appendChild(stylesheet);
for (const rule of styleRules) {
stylesheet.sheet.insertRule(rule);
}
Enfin, la feuille de style est supprimée du DOM dans la fonction de nettoyage.
return () => {
document.head.removeChild(stylesheet);
};
Maintenant, nous pouvons utiliser notre useStyles
crochet, créez donc un nouveau fichier appelé UseInsertionEffectExample.jsx
et copiez-y le code ci-dessous.
src/exemples/UseInsertionEffectExample.jsx
import { useState } from "react";
import { useStyles } from "./useStyles";
const styles = props => {
return {
buttonsContainer: {
display: "flex",
flexDirection: "column",
gap: "1rem",
},
button: {
backgroundColor: "#9333ea",
color: "#faf5ff",
fontSize: "18px",
padding: "8px 12px",
width: `${props.width}px`,
},
};
};
const UseInsertionEffectExample = props => {
const [width, setWidth] = useState(150);
const style = useStyles(styles, { width });
return (
<div>
<h2 className="text-xl font-bold mb-4">useInsertionEffect Example</h2>
<div>
<div className={style.buttonsContainer}>
<button
className={style.button}
onClick={() => setWidth(width => width - 5)}
>
Decrement
</button>
<button
className={style.button}
onClick={() => setWidth(width => width + 5)}
>
Increment
</button>
</div>
</div>
</div>
);
};
export default UseInsertionEffectExample;
Nous avons le styles
objet avec des styles pour les boutons qui sont ensuite passés au useStyles
accrocher. La useStyles
hook renvoie un objet avec des classes qui ressemblent à ceci :
{
"buttonsContainer": "buttonsContainer_slsoSY55FjCzpn0fbFdjA",
"button": "button_YfGnVuvh3ESCaIZdEVwNr"
}
La buttonsContainer
la classe est transmise à un div
élément, tandis que le button
classe à Decrement
et Increment
boutons.
La width
l’état change chaque fois que l’un des boutons est cliqué. Lorsque cela se produit, les anciens styles sont supprimés et de nouveaux sont créés et insérés à nouveau dans le DOM à l’intérieur du useInsertionEffect
.
Ensuite, nous devons rendre le UseInsertionEffectExample
composant.
src/App.jsx
import "./App.css";
import UseInsertionEffectExample from "./examples/UseInsertionEffectExample";
function App() {
return (
<div className="App space-y-16">
<UseInsertionEffectExample />
</div>
);
}
export default App;
Le GIF ci-dessous montre ce que UseInsertionEffectExample
devrait ressembler.
Sommaire
Nous avons couvert les nouveaux crochets qui ont été ajoutés dans React 18. Chose intéressante, vous pourriez en fait ne pas en utiliser aucun.
useSyncExternalStore
et useInsertionEffect
sont spécialement conçus pour les auteurs de bibliothèques qui travaillent sur la gestion d’état et les solutions CSS-in-JS. La useId
hook est utile si votre application React s’exécute à la fois sur le client et sur le serveur, donc si votre application React s’exécute uniquement côté client, vous n’en aurez pas vraiment besoin.
Par ailleurs, useTransition
et useDeferredValue
peut être utilisé pour marquer certaines mises à jour d’état comme moins importantes et différer, mais ce n’est pas quelque chose dont toutes les applications auront nécessairement besoin.
Néanmoins, tous ces crochets sont un excellent ajout, et j’ai hâte de voir ce que React apportera d’autre à l’avenir. Si vous souhaitez en savoir plus sur React 18, assurez-vous de lire le blog officiel qui couvre de nouvelles fonctionnalités.
Source link