Fermer

octobre 4, 2022

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.

useId Exemple a des entrées pour le nom et l'âge, avec des identifiants uniques générés pour chacun

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.

useDeferredValue Example - La fonctionnalité de recherche de repas montre que l'utilisateur tape

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é namenous 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 change
  • setState – met à jour l’état de manière immuable en utilisant immer et notifie tous les abonnés
  • getState – renvoie l’état actuel
  • useStore – un emballage autour useSyncExternalStore 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.

L'exemple useSyncExternalStore montre les boutons de décrémentation, d'incrémentation, de division et de multiplication affectant le nombre et le nombre multiplié.

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 useMemoun 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 useInsertionEffectnous 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.

exemple useInsertionEffect montrant les boutons de décrémentation et d'incrémentation modifiant la largeur des boutons

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

octobre 4, 2022