Pourquoi suis-je déçu par les crochets

La traduction de l'article a été préparée en prévision du début du cours "React.js Developer" .










Comment les hameçons sont-ils utiles?



Avant de vous dire quoi et pourquoi j'ai été déçu, je tiens à déclarer officiellement qu'en fait, je suis fan de crochets .



J'entends souvent que les hooks sont créés pour remplacer les composants de classe. Malheureusement, le post "Introduction aux Hooks" publié sur le site officiel de React annonce cette innovation, franchement, malheureuse: les



Hooks sont une innovation de React 16.8 qui vous permet d'utiliser l'état et d'autres fonctionnalités de React sans écrire de classes.



Le message que je vois ici ressemble à ceci: "Les classes ne sont pas cool!". Pas assez pour vous motiver à utiliser des crochets. À mon avis, les hooks nous permettent de résoudre les problèmes de fonctionnalités transversales de manière plus élégante que les approches précédentes: mixins , composants d'ordre supérieur et accessoires de rendu .



Les fonctions de journalisation et d'authentification sont communes à tous les composants et les hooks vous permettent d'attacher de telles fonctions réutilisables aux composants.



Quel est le problème avec les composants de classe?



Il y a une beauté incompréhensible dans un composant sans état (c'est-à-dire un composant sans état interne) qui prend des accessoires en entrée et renvoie un élément React. C'est une fonction pure, c'est-à-dire une fonction sans effets secondaires.



export const Heading: React.FC<HeadingProps> = ({ level, className, tabIndex, children, ...rest }) => {
  const Tag = `h${level}` as Taggable;

  return (
    <Tag className={cs(className)} {...rest} tabIndex={tabIndex}>
      {children}
    </Tag>
  );
};


Malheureusement, l'absence d'effets secondaires limite l'utilisation de composants sans état. Après tout, la manipulation de l'État est essentielle. Dans React, cela signifie que des effets secondaires sont ajoutés aux beans de classe qui sont avec état. Ils sont également appelés composants de conteneur. Ils exécutent des effets secondaires et transmettent des accessoires à de pures fonctions sans état.



Il existe des problèmes bien connus avec les événements de cycle de vie basés sur les classes. Beaucoup de gens sont mécontents de devoir répéter la logique des méthodes componentDidMountet componentDidUpdate.



async componentDidMount() {
  const response = await get(`/users`);
  this.setState({ users: response.data });
};

async componentDidUpdate(prevProps) {
  if (prevProps.resource !== this.props.resource) {
    const response = await get(`/users`);
    this.setState({ users: response.data });
  }
};


Tôt ou tard, tous les développeurs sont confrontés à ce problème.



Ce code d'effet secondaire peut être exécuté dans un seul composant à l'aide d'un crochet d'effet.



const UsersContainer: React.FC = () => {
  const [ users, setUsers ] = useState([]);
  const [ showDetails, setShowDetails ] = useState(false);

 const fetchUsers = async () => {
   const response = await get('/users');
   setUsers(response.data);
 };

 useEffect( () => {
    fetchUsers(users)
  }, [ users ]
 );

 // etc.


Hook useEffectrend la vie beaucoup plus facile, mais il prive de cette fonction pure - composant sans état - que nous utilisions auparavant. C'est la première chose qui m'a déçu.



Un autre paradigme JavaScript à connaître



J'ai 49 ans et je suis fan de React. Après avoir développé une application de braises avec cet observateur et la folie des propriétés calculées, j'aurai toujours un sentiment affectueux pour le flux de données unidirectionnel.



Le problème avec les hooks useEffectet autres est qu'ils ne sont utilisés nulle part ailleurs dans le paysage JavaScript. Il est inhabituel et généralement bizarre. Je ne vois qu'une seule façon de l'apprivoiser: utiliser ce crochet dans la pratique et souffrir. Et aucun exemple de compteurs ne m'incitera à coder de façon désintéressée toute la nuit. Je suis freelance et j'utilise non seulement React mais aussi d'autres bibliothèques, et je suis déjà fatiguégarder une trace de toutes ces innovations. Dès que je pense que je dois installer le plugin eslint, qui me mettra sur la bonne voie, ce nouveau paradigme commence à me fatiguer.



Les tableaux de dépendances sont l'enfer



Le hook useEffect peut prendre un deuxième argument facultatif appelé tableau de dépendances , qui vous permet de rappeler l'effet lorsque vous en avez besoin. Pour déterminer si une modification s'est produite , React compare les valeurs entre elles à l'aide de la méthode Object.is . Si des éléments ont changé depuis le dernier cycle de rendu, l'effet sera appliqué aux nouvelles valeurs.



La comparaison est idéale pour gérer les types de données primitifs. Mais si l'un des éléments est un objet ou un tableau, des problèmes peuvent survenir. Object.is compare les objets et les tableaux par référence et vous ne pouvez rien y faire. L'algorithme de comparaison personnalisé ne peut pas être appliqué.



La validation des objets par référence est une pierre d'achoppement connue. Jetons un coup d'œil à une version simplifiée d'un problème que j'ai récemment rencontré.



const useFetch = (config: ApiOptions) => {
  const  [data, setData] = useState(null);

  useEffect(() => {
    const { url, skip, take } = config;
    const resource = `${url}?$skip=${skip}&take=${take}`;
    axios({ url: resource }).then(response => setData(response.data));
  }, [config]); // <-- will fetch on each render

  return data;
};

const App: React.FC = () => {
  const data = useFetch({ url: "/users", take: 10, skip: 0 });
  return <div>{data.map(d => <div>{d})}</div>;
};


À la ligne 14 , un useFetchnouvel objet sera passé à la fonction pour chaque rendu, sauf si nous faisons en sorte que le même objet soit utilisé à chaque fois. Dans ce scénario, vous souhaitez vérifier les champs de l'objet, pas la référence à celui-ci.



Je comprends pourquoi React ne fait pas de comparaisons d'objets approfondies comme cette solution . Par conséquent, vous devez utiliser le hook avec précaution, sinon de graves problèmes de performances de l'application peuvent survenir. Je me demande constamment ce que l'on peut faire à ce sujet et j'ai déjà trouvé plusieurs options . Pour des objets plus dynamiques, vous devrez rechercher plus de solutions de contournement.



Il existe un plugin eslint pour corriger les erreurs automatiquementtrouvé lors de la validation du code. Il convient à tout éditeur de texte. Pour être honnête, je suis agacé par toutes ces nouvelles fonctionnalités qui nécessitent l'installation d'un plugin externe pour les tester.



L'existence même de plugins tels que use-deep-object-compare et use-memo-one suggère qu'il y a vraiment un problème (ou du moins un gâchis).



React repose sur l'ordre dans lequel les hooks sont appelés



Les premiers hooks personnalisés étaient plusieurs implémentations d'une fonction useFetchpermettant de faire des requêtes à une API distante. La plupart d'entre eux ne résolvent pas le problème des requêtes API distantes à partir d'un gestionnaire d'événements, car les hooks ne peuvent être utilisés qu'au début d'un composant fonctionnel.



Mais que se passe-t-il s'il y a des liens vers des sites paginés dans les données et que nous voulons réexécuter l'effet lorsque l'utilisateur clique sur le lien? Voici un cas d'utilisation simple useFetch:



const useFetch = (config: ApiOptions): [User[], boolean] => {
  const [data, setData] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const { skip, take } = config;

    api({ skip, take }).then(response => {
      setData(response);
      setLoading(false);
    });
  }, [config]);

  return [data, loading];
};

const App: React.FC = () => {
  const [currentPage, setCurrentPage] = useState<ApiOptions>({
    take: 10,
    skip: 0
  });

  const [users, loading] = useFetch(currentPage);

  if (loading) {
    return <div>loading....</div>;
  }

  return (
    <>
      {users.map((u: User) => (
        <div>{u.name}</div>
      ))}
      <ul>
        {[...Array(4).keys()].map((n: number) => (
          <li>
            <button onClick={() => console.log(' ?')}>{n + 1}</button>
          </li>
        ))}
      </ul>
    </>
  );
};


À la ligne 23, le hook useFetchsera appelé une fois lors du premier rendu. Aux lignes 35 à 38, nous rendons les boutons de pagination. Mais comment appeler le hook useFetchdu gestionnaire d'événements pour ces boutons?



Les règles de hook indiquent clairement:



n'utilisez pas de hooks dans des boucles, des conditionnelles ou des fonctions imbriquées; à la place, utilisez toujours des hooks uniquement au niveau supérieur des fonctions React.



Les hooks sont appelés dans le même ordre chaque fois que le composant est rendu. Il y a plusieurs raisons à cela, que vous pouvez découvrir dans cet excellent article.



Vous ne pouvez pas faire ça:



<button onClick={() => useFetch({ skip: n + 1 * 10, take: 10 })}>
  {n + 1}
</button>


L'appel d'un hook à useFetchpartir d'un gestionnaire d'événements enfreint les règles des hooks, car l'ordre de leur appel change à chaque rendu.



Renvoyer une fonction exécutable à partir d'un hook



Je connais deux solutions à ce problème. Ils adoptent la même approche et j'aime les deux. Le plugin react-async-hook renvoie une fonction du hook execute:



import { useAsyncCallback } from 'react-async-hook';

const AppButton = ({ onClick, children }) => {
  const asyncOnClick = useAsyncCallback(onClick);
  return (
    <button onClick={asyncOnClick.execute} disabled={asyncOnClick.loading}>
      {asyncOnClick.loading ? '...' : children}
    </button>
  );
};

const CreateTodoButton = () => (
  <AppButton
    onClick={async () => {
      await createTodoAPI('new todo text');
    }}
  >
    Create Todo
  </AppButton>
);


L'appel du hook useAsyncCallbackrenverra un objet avec les propriétés de charge, d'erreur et de résultat attendues, ainsi qu'une fonction executequi peut être appelée à partir d'un gestionnaire d'événements.



React-hooks-async est un plugin avec une approche similaire. Il utilise une fonction useAsyncTask.



Voici un exemple complet avec une version simplifiée useAsyncTask:



const createTask = (func, forceUpdateRef) => {
  const task = {
    start: async (...args) => {
      task.loading = true;
      task.result = null;
      forceUpdateRef.current(func);
      try {
        task.result = await func(...args);
      } catch (e) {
        task.error = e;
      }
      task.loading = false;
      forceUpdateRef.current(func);
    },
    loading: false,
    result: null,
    error: undefined
  };
  return task;
};

export const useAsyncTask = (func) => {
  const forceUpdate = useForceUpdate();
  const forceUpdateRef = useRef(forceUpdate);
  const task = useMemo(() => createTask(func, forceUpdateRef), [func]);

  useEffect(() => {
    forceUpdateRef.current = f => {
      if (f === func) {
        forceUpdate({});
      }
    };
    const cleanup = () => {
      forceUpdateRef.current = () => null;
    };
    return cleanup;
  }, [func, forceUpdate]);

  return useMemo(
    () => ({
      start: task.start,
      loading: task.loading,
      error: task.error,
      result: task.result
    }),
    [task.start, task.loading, task.error, task.result]
  );
};


La fonction createTask renvoie un objet de tâche sous la forme suivante.



interface Task {
  start: (...args: any[]) => Promise<void>;
  loading: boolean;
  result: null;
  error: undefined;
}


Le travail a des états , et , auxquels nous nous attendons. Mais la fonction renvoie également une fonction startqui peut être appelée ultérieurement. Le travail créé avec la fonction createTaskn'affecte pas la mise à jour. La mise à jour est déclenchée par des fonctions forceUpdateet forceUpdateRefdans useAsyncTask.



Nous avons maintenant une fonction startqui peut être appelée à partir d'un gestionnaire d'événements ou d'un autre morceau de code, pas nécessairement depuis le début d'un composant fonctionnel.



Mais nous avons perdu la possibilité d'appeler le hook lors de la première exécution du composant fonctionnel. C'est bien que le plugin react-hooks-async contienne une fonction useAsyncRun- cela facilite les choses:



export const useAsyncRun = (
  asyncTask: ReturnType<typeof useAsyncTask>,
  ...args: any[]
) => {
  const { start } = asyncTask;
  useEffect(() => {
    start(...args);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [asyncTask.start, ...args]);
  useEffect(() => {
    const cleanup = () => {
      //   
    };
    return cleanup;
  });
};


La fonction startsera exécutée lorsque l'un des arguments change args. Maintenant, le code avec des crochets ressemble à ceci:



const App: React.FC = () => {
  const asyncTask = useFetch(initialPage);
  useAsyncRun(asyncTask);

  const { start, loading, result: users } = asyncTask;

  if (loading) {
    return <div>loading....</div>;
  }

  return (
    <>
      {(users || []).map((u: User) => (
        <div>{u.name}</div>
      ))}

      <ul>
        {[...Array(4).keys()].map((n: number) => (
          <li key={n}>
            <button onClick={() => start({ skip: 10 * n, take: 10 })}>
              {n + 1}
            </button>
          </li>
        ))}
      </ul>
    </>
  );
};


Selon les règles des hooks, nous utilisons un hook useFetchau début d'un composant fonctionnel. La fonction useAsyncRunappelle l'API au tout début, et startnous utilisons la fonction du gestionnaire onClickpour les boutons de pagination.



Maintenant, le crochet useFetchpeut être utilisé aux fins prévues, mais, malheureusement, vous devez suivre des solutions de contournement. Nous utilisons également une fermeture, ce qui, je dois l'avouer, me fait un peu peur.



Contrôle des hooks dans les programmes d'application



Dans les programmes d'application, tout doit fonctionner comme prévu. Si vous prévoyez de suivre les problèmes liés aux composants ET les interactions des utilisateurs avec des composants spécifiques, vous pouvez utiliser LogRocket .







LogRocket est une sorte d'enregistreur vidéo d'application Web qui enregistre presque tout ce qui se passe sur le site. Le plugin LogRocket pour React vous permet de trouver des sessions utilisateur au cours desquelles l'utilisateur a cliqué sur un composant spécifique de votre application. Vous comprendrez comment les utilisateurs interagissent avec les composants et pourquoi certains composants ne rendent rien.



LogRocket enregistre toutes les actions et tous les états du magasin Redux. C'est un ensemble d'outils pour votre application qui vous permet d'enregistrer les demandes / réponses avec des en-têtes et des corps. Ils écrivent du HTML et du CSS sur la page, fournissant un rendu pixel par pixel pour les applications monopages les plus complexes.



LogRocket offre une approche moderne du débogage des applications React - essayez-le gratuitement .



Conclusion



Je pense que l'exemple c useFetchexplique le mieux pourquoi je suis frustré par les crochets.



Obtenir le résultat souhaité ne s'est pas avéré aussi facile que prévu, mais je comprends toujours pourquoi il est si important d'utiliser des crochets dans un ordre spécifique. Malheureusement, nos capacités sont sévèrement limitées en raison du fait que les hooks ne peuvent être appelés qu'au début d'un composant fonctionnel, et nous devrons chercher des solutions de contournement. La solution est useFetchassez compliquée. De plus, lorsque vous utilisez des crochets, vous ne pouvez pas vous passer de fermetures. Les fermetures sont des surprises continues qui ont laissé de nombreuses cicatrices dans mon âme.



Fermetures (telles que celles transmises à useEffectetuseCallback) peut récupérer d'anciennes versions d'accessoires et de valeurs d'état. Cela se produit, par exemple, lorsqu'une des variables capturées est manquante dans le tableau d'entrée pour une raison quelconque - des difficultés peuvent survenir.



L'état obsolète qui se produit après l'exécution de code dans une fermeture est l'un des problèmes que le hook linter est conçu pour résoudre. Il y a beaucoup de questions sur Stack Overflow sur les hooks obsolètes useEffectet autres. J'ai encapsulé des fonctions useCallbacket tordu les tableaux de dépendances de cette façon et cela pour se débarrasser de l'état périmé ou du problème de répétition infinie. Il ne peut pas en être autrement, mais c'est un peu ennuyeux. C'est un vrai problème que vous devez résoudre pour prouver votre valeur.



Au début de ce post, j'ai dit qu'en général j'aime les crochets. Mais ils semblent très compliqués. Il n'y a rien de tel dans le paysage JavaScript actuel. L'appel de hooks chaque fois qu'un composant fonctionnel est rendu crée des problèmes que les mixins ne font pas. Le besoin d'un linter pour utiliser ce modèle n'est pas très crédible et les fermetures sont un problème.



J'espère que j'ai mal compris cette approche. Si c'est le cas, écrivez à ce sujet dans les commentaires.





Lire la suite:






All Articles