Crochets personnalisés. Partie 1





Bonne journée, mes amis!



Je présente à votre attention les dix meilleurs crochets personnalisés .



Table des matières







useMemoCompare



Ce hook est similaire à useMemo, mais au lieu d'un tableau de dépendances, il reçoit une fonction qui compare les valeurs précédentes et nouvelles. Une fonction peut comparer des propriétés imbriquées, appeler des méthodes sur des objets ou faire autre chose à des fins de comparaison. Si la fonction renvoie true, le hook renvoie une référence à l'ancien objet. Il est à noter que ce hook, contrairement à useMemo, n'implique pas l'absence de calculs complexes répétés. Il doit transmettre la valeur calculée pour comparaison. Cela peut être utile lorsque vous souhaitez partager la bibliothèque avec d'autres développeurs et que vous ne voulez pas les forcer à se souvenir de l'objet avant de le soumettre. Si un objet est créé dans le corps d'un composant (dans le cas où il dépend d'accessoires), il sera nouveau à chaque fois qu'il est rendu. Si l'objet est une dépendance de useEffect alors l'effet sera déclenché sur chaque rendu,ce qui peut entraîner des problèmes, jusqu'à une boucle sans fin. Ce hook vous permet d'éviter ce développement d'événements en utilisant l'ancienne référence d'objet au lieu de la nouvelle si la fonction reconnaît les objets comme étant les mêmes.



import React, { useState, useEffect, useRef } from "react";

// 
function MyComponent({ obj }) {
  const [state, setState] = useState();

  //   ,   "id"  
  const objFinal = useMemoCompare(obj, (prev, next) => {
    return prev && prev.id === next.id;
  });

  //       objFinal
  //    obj ,   ,  obj  
  //     ,        
  //   ,       ,     
  //   ->      ->    ->  ..
  useEffect(() => {
    //       
    return objFinal.someMethod().then((value) => setState(value));
  }, [objFinal]);

  //     [obj.id]   ?
  useEffect(() => {
    // eslint-plugin-hooks  ,  obj     
    //     eslint-disable-next-line    
    //           
    return obj.someMethod().then((value) => setState(value));
  }, [obj.id]);
}

// 
function useMemoCompare(next, compare) {
  // ref    
  const prevRef = useRef();
  const prev = prevRef.current;

  //       
  //    
  const isEqual = compare(prev, next);

  //    ,  prevRef
  //       
  // ,    true,    
  useEffect(() => {
    if (!isEqual) {
      prevRef.current = next;
    }
  });

  //   ,   
  return isEqual ? prev : next;
}


useAsync



Il est recommandé d'afficher l'état d'une requête asynchrone. Un exemple

serait de récupérer des données à partir d'une API et d'afficher un indicateur de chargement avant de rendre les résultats. Un autre exemple consiste à désactiver un bouton pendant que le formulaire est soumis, puis à afficher le résultat. Au lieu de polluer le composant avec de nombreux appels useState pour suivre l'état de la fonction asynchrone, nous pouvons utiliser ce hook, qui prend une fonction asynchrone et renvoie la valeur, l'erreur et l'état si nécessaire pour mettre à jour l'interface utilisateur. Les valeurs possibles pour la propriété "status" sont "idle", "pending", "success" et "error". Notre hook vous permet d'exécuter une fonction immédiatement ou tardivement à l'aide de la fonction execute.



import React, { useState, useEffect, useCallback } from 'react'

// 
function App() {
  const {execute, status, value, error } = useAsync(myFunction, false)

  return (
    <div>
      {status === 'idle' && <div>     </div>}
      {status === 'success' && <div>{value}</div>}
      {status === 'error' && <div>{error}</div>}
      <button onClick={execute} disabled={status === 'pending'}>
        {status !== 'pending' ? ' ' : '...'}
      </button>
    </div>
  )
}

//     
//    50% 
const myFunction = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const random = Math.random() * 10
      random <=5
        ? resolve(' ')
        : reject(' ')
    }, 2000)
  })
}

// 
const useAsync = (asyncFunction, immediate = true) => {
  const [status, setStatus] = useState('idle')
  const [value, setValue] = useState(null)
  const [error, setError] = useState(null)

  //  "execute"  asyncFunction 
  //     pending, value  error
  // useCallback   useEffect   
  // useEffect     asyncFunction
  const execute = useCallback(() => {
    setStatus('pending')
    setValue(null)
    setError(null)

    return asyncFunction()
      .then(response => {
        setValue(response)
        setStatus('success')
      })
      .catch(error => {
        setError(error)
        setStatus('error')
      })
  }, [asyncFunction])

  //  execute   
  //   , execute    
  // ,    
  useEffect(() => {
    if (immediate) {
      execute()
    }
  }, [execute, immediate])

  return { execute, status, value, error }
}


useRequireAuth



Le but de ce hook est de rediriger l'utilisateur vers la page de connexion lors de la déconnexion du compte. Notre hook est une composition des hooks "useAuth" et "useRouter". Bien sûr, nous pouvons implémenter la fonctionnalité requise dans le hook useAuth, mais nous devons ensuite l'inclure dans le schéma de routage. Avec la composition, nous pouvons simplifier useAuth et useRouter en implémentant une redirection avec un hook personnalisé.



import Dashboard from "./Dahsboard.js";
import Loading from "./Loading.js";
import { useRequireAuth } from "./use-require-auth.js";

function DashboardPage(props) {
  const auth = useRequireAuth();

  //   auth  null (   )
  //  false (    )
  //   
  if (!auth) {
    return <Loading />;
  }

  return <Dashboard auth={auth} />;
}

//  (use-require-auth.js)
import { useEffect } from "react";
import { useAuth } from "./use-auth.js";
import { useRouter } from "./use-router.js";

function useRequireAuth(redirectUrl = "./signup") {
  const auth = useAuth();
  const router = useRouter();

  //   auth.user  false,
  // ,   ,  
  useEffect(() => {
    if (auth.user === false) {
      router.push(redirectUrl);
    }
  }, [auth, router]);

  return auth;
}


useRouter



Si vous utilisez React Router dans votre travail, vous avez peut-être remarqué que plusieurs hooks utiles sont apparus récemment, tels que "useParams", "useLocation", "useHistory" et "useRouterMatch". Essayons de les regrouper dans un seul hook qui renvoie les données et les méthodes dont nous avons besoin. Nous allons vous montrer comment combiner plusieurs hooks et renvoyer un seul objet contenant leurs états. Pour les bibliothèques comme React Router, il est judicieux de fournir une sélection du hook souhaité. Cela évite les rendus inutiles. Mais parfois, nous avons besoin de la totalité ou de la plupart des hooks nommés.



import { useMemo } from "react";
import {
  useParams,
  useLocation,
  useHistory,
  useRouterMatch,
} from "react-router-dom";
import queryString from "query-string";

// 
function MyComponent() {
  //   
  const router = useRouter();

  //     (?postId=123)    (/:postId)
  console.log(router.query.postId);

  //    
  console.log(router.pathname);

  //     router.push()
  return <button onClick={(e) => router.push("./about")}>About</button>;
}

// 
export function useRouter() {
  const params = useParams();
  const location = useLocation();
  const history = useHistory();
  const match = useRouterMatch();

  //    
  //    ,        
  return useMemo(() => {
    return {
      //    push(), replace()  pathname   
      push: history.push,
      replace: history.replace,
      pathname: location.pathname,
      //          "query"
      //  ,    
      // : /:topic?sort=popular -> { topic: 'react', sort: 'popular' }
      query: {
        ...queryString.parse(location.search), //    
        ...params,
      },
      //   "match", "location"  "history"
      //     React Router
      match,
      location,
      history,
    };
  }, [params, match, location, history]);
}


useAuth



Il est courant d'avoir plusieurs composants qui sont rendus selon que l'utilisateur est connecté ou non à un compte. Certains de ces composants appellent des méthodes d'authentification telles que la connexion, la déconnexion, sendPasswordResetEmail, etc. Le hook "useAuth" est parfait pour cela, ce qui garantit que le composant reçoit l'état d'authentification et redessine le composant lorsque des modifications sont présentes. Au lieu d'instancier useAuth pour chaque utilisateur, notre hook appelle useContext pour obtenir des données du composant parent. La vraie magie se produit dans le composant provideAuth, où toutes les méthodes d'authentification (dans l'exemple que nous utilisons Firebase) sont encapsulées dans un hook useProvideAuth. Le contexte est ensuite utilisé pour transmettre l'objet d'authentification actuel aux composants enfants appelant useAuth.Cela aura plus de sens après avoir lu l'exemple. Une autre raison pour laquelle j'aime ce hook est qu'il fait abstraction du véritable fournisseur d'authentification (Firebase), ce qui facilite les modifications.



//   App
import React from "react";
import { ProvideAuth } from "./use-auth.js";

function App(props) {
  return (
    <ProvideAuth>
      {/*
           ,     
          Next.js,    : /pages/_app.js
      */}
    </ProvideAuth>
  );
}

//  ,    
import React from "react";
import { useAuth } from "./use-auth.js";

function NavBar(props) {
  //   auth      
  const auth = useAuth();

  return (
    <NavbarContainer>
      <Logo />
      <Menu>
        <Link to="/about">About</Link>
        <Link to="/contact">Contact</Link>
        {auth.user ? (
          <Fragment>
            <Link to="/account">Account ({auth.user.email})</Link>
            <Button onClick={() => auth.signout()}>Signout</Button>
          </Fragment>
        ) : (
          <Link to="/signin">Signin</Link>
        )}
      </Menu>
    </NavbarContainer>
  );
}

//  (use-auth.js)
import React, { useState, useEffect, useContext, createContext } from "react";
import * as firebase from "firebase/app";
import "firebase/auth";

//    Firebase
firebase.initializeApp({
  apiKey: "",
  authDomain: "",
  projectId: "",
  appID: "",
});

const authContext = createContext();

//  Provider,      "auth"
//     ,  useAuth
export const useAuth = () => {
  return useContext(authContext);
};

//        "auth"
//      
export const useAuth = () => {
  return useContext(authContext);
};

//  ,   "auth"    
function useProviderAuth() {
  const [user, setUser] = useState(null);

  //    Firebase,   
  //  
  const signin = (email, password) => {
    return firebase
      .auth()
      .signInWithEmailAndPassword(email, password)
      .then((response) => {
        setUser(response.user);
        return response.user;
      });
  };

  const signup = (email, password) => {
    return firebase
      .auth()
      .createUserWithEmailAndPassword(email, password)
      .then((response) => {
        setUser(response.user);
        return response.user;
      });
  };

  const signout = () => {
    return firebase
      .auth()
      .signOut()
      .then(() => {
        setUser(false);
      });
  };

  const sendPasswordResetEmail = (email) => {
    return firebase
      .auth()
      .sendPasswordResetEmail(email)
      .then(() => true);
  };

  const confirmPasswordReset = (code, password) => {
    return firebase
      .auth()
      .confirmPasswordReset(code, password)
      .then(() => true);
  };

  //    
  //       
  //   ,  
  //      "auth"
  useEffect(() => {
    const unsubscribe = firebase.auth().onAuthStateChange((user) => {
      if (user) {
        setUser(user);
      } else {
        setUser(false);
      }
    });

    //   
    return () => unsubscribe();
  }, []);

  //   "user"   
  return {
    user,
    signin,
    signup,
    signout,
    sendPasswordResetEmail,
    confirmPasswordReset,
  };
}


useEventListener



Si vous devez gérer un grand nombre de gestionnaires d'événements qui s'enregistrent avec useEffect, vous pouvez les séparer en hooks séparés. Dans l'exemple ci-dessous, nous créons un hook useEventListener qui vérifie la prise en charge de addEventListener, ajoute des gestionnaires et les supprime à la sortie.

import { useState, useRef, useEffect, useCallback } from "react";

// 
function App() {
  //     
  const [coords, setCoords] = useState({ x: 0, y: 0 });

  //     useCallback,
  //     
  const handler = useCallback(
    ({ clientX, clientY }) => {
      //  
      setCoords({ x: clientX, y: clientY });
    },
    [setCoords]
  );

  //      
  useEventListener("mousemove", handler);

  return <h1> : ({(coords.x, coords.y)})</h1>;
}

// 
function useEventListener(eventName, handler, element = window) {
  //  ,  
  const saveHandler = useRef();

  //  ref.current   
  //          
  //      
  //      
  useEffect(() => {
    saveHandler.current = handler;
  }, [handler]);

  useEffect(
    () => {
      //   addEventListener
      const isSupported = element && element.addEventListener;
      if (!isSupported) return;

      //   ,   ,   ref
      const eventListener = (event) => saveHandler.current(event);

      //   
      element.addEventListener(eventName, eventListener);

      //     
      return () => {
        element.removeEventListener(eventName, eventListener);
      };
    },
    [eventName, element] //     
  );
}


useWhyDidYouUpdate



Ce hook vous permet de déterminer quels changements d'accessoires conduisent à un nouveau rendu. Si la fonction est «complexe» et que vous êtes sûr qu'elle est propre, c.-à-d. renvoie les mêmes résultats pour les mêmes accessoires, vous pouvez utiliser le composant d'ordre supérieur "React.memo" comme nous le faisons dans l'exemple ci-dessous. Si, après cela, les rendus inutiles ne se sont pas arrêtés, vous pouvez utiliser useWhyDidYouUpdate, qui renvoie à la console les accessoires qui changent pendant le rendu, en indiquant les valeurs précédentes et actuelles.



import { useState, useEffect, useRef } from "react";

// ,  <Counter>     
//      React.memo,   
//   useWhyDidYouUpdate   
const Counter = React.memo((props) => {
  useWhyDidYouUpdate("Counter", props);
  return <div style={props.style}>{props.count}</div>;
});

function App() {
  const [count, setCount] = useState(0);
  const [userId, setUserId] = useState(0);

  //  ,  ,    <Counter>
  //    ,       userId
  //   "switch user". ,   
  //       
  //    ,      
  //    
  const counterStyle = {
    fontSize: "3rem",
    color: "red",
  };
}

return (
  <div>
    <div className="counter">
      <Counter count={count} style={counterStyle} />
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
    <div className="user">
      <img src={`http://i.pravatar.cc/80?img=${userId}`} />
      <button onClick={() => setUserId(userId + 1)}>Switch User</button>
    </div>
  </div>
);

// 
function useWhyDidYouUpdate(name, props) {
  //    "ref"   
  //        
  const prevProps = useRef();

  useEffect(() => {
    if (prevProps.current) {
      //      
      const allKeys = Object.keys({ ...prevProps.current, ...props });
      //       
      const changesObj = {};
      //  
      allKeys.forEach((key) => {
        //     
        if (prevProps.current[key] !== props[key]) {
          //    changesObj
          changesObj[key] = {
            from: prevProps.current[key],
            to: props[key],
          };
        }
      });

      //   changesObj - ,    
      if (object.keys(changesObj).length) {
        console.log("why-did-you-update", name, changesObj);
      }
    }

    // ,  prevProps      
    prevProps.current = props;
  });
}


useDarkMode



Ce hook implémente la logique pour changer le jeu de couleurs du site (clair et foncé). Il utilise le stockage local pour stocker le modèle sélectionné par l'utilisateur, le mode par défaut défini dans le navigateur à l'aide de la requête multimédia "prefers-color-scheme". Pour activer le mode sombre, utilisez la classe "dark-mode" de l'élément "body". Hook démontre également le pouvoir de la composition. La synchronisation d'état avec localStorage est implémentée en utilisant le hook "useLocalStorage" et en définissant le schéma préféré de l'utilisateur en utilisant le hook "useMedia", qui sont conçus à des fins différentes. Cependant, la composition de ces hooks se traduit par un hook encore plus puissant de quelques lignes de code seulement. C'est presque le même que le pouvoir "compositionnel" des hooks par rapport à l'état du composant.



function App() {
  const [darkMode, setDarkMode] = useDarkMode();

  return (
    <div>
      <div className="navbar">
        <Toggle darkMode={darkMode} setDarkMode={setDarkMode} />
      </div>
      <Content />
    </div>
  );
}

// 
function useDarkMode() {
  //   "useLocalStorage"   
  const [enabledState, setEnableState] = useLocalStorage("dark-mode-enabled");

  //      
  //   "usePrefersDarkMode"   "useMedia"
  const prefersDarkMode = usePrefersDarkMode();

  //  enabledState ,  , ,  prefersDarkMode
  const enabled =
    typeof enabledState !== "undefined" ? enabledState : prefersDarkMode;

  //   / 
  useEffect(
    () => {
      const className = "dark-mode";
      const element = window.document.body;
      if (enabled) {
        element.classList.add(className);
      } else {
        element.classList.remove(className);
      }
    },
    [enabled] //      enabled
  );

  //     
  return [enabled, setEnableState];
}

//   "useMedia"    
//      ,    ,
//       -   
//        
function usePrefersDarkMode() {
  return useMedia(["(prefers-color-scheme: dark)"], [true], false);
}


useMedia



Ce hook encapsule la logique de définition des requêtes multimédias. Dans l'exemple ci-dessous, nous rendons un nombre différent de colonnes en fonction de la requête multimédia en fonction de la largeur d'écran actuelle, puis plaçons une image au-dessus des colonnes afin qu'elle nivelle la différence de hauteur de colonne (nous ne voulons pas qu'une colonne soit plus haute que l'autre) ... Vous pouvez créer un hook qui détermine directement la largeur de l'écran, mais notre hook vous permet de combiner des requêtes multimédias spécifiées dans JS et une feuille de style.



import { useState, useEffect } from "react";

function App() {
  const columnCount = useMedia(
    // -
    ["(min-width: 1500px)", "(min-width: 1000px)", "(min-width: 600px)"],
    //     
    [5, 4, 3],
    //    
    2
  );

  //      (  0)
  let columnHeight = new Array(columnCount).fill(0);

  //   ,   
  let columns = new Array(columnCount).fill().map(() => []);

  data.forEach((item) => {
    //     
    const shortColumntIndex = columnHeight.indexOf(Math.min(...columnHeight));
    //  
    columns[shortColumntIndex].push(item);
    //  
    columnHeight[shortColumntIndex] += item.height;
  });

  //    
  return (
    <div className="App">
      <div className="columns is-mobile">
        {columns.map((column) => (
          <div className="column">
            {column.map((item) => (
              <div
                className="image-container"
                style={{
                  //     aspect ratio
                  paddingTop: (item.height / item.width) * 100 + "%",
                }}
              >
                <img src={item.image} alt="" />
              </div>
            ))}
          </div>
        ))}
      </div>
    </div>
  );
}

// 
function useMedia(queries, values, defaultValue) {
  //   -
  const mediaQueryList = queries.map((q) => window.matchMedia(q));

  //      
  const getValue = () => {
    //     
    const index = mediaQueryList.findIndex((mql) => mql.matches);
    //       
    return typeof values[index] !== "undefined"
      ? values[index]
      : defaultValue;
  };

  //      
  const [value, setValue] = useState(getValue);

  useEffect(
    () => {
      //   
      //  :  getValue   useEffect,  
      //       
      //        
      const handler = () => setValue(getValue);
      //     -
      mediaQueryList.forEach((mql) => mql.addEventListener(handler));
      //    
      return () =>
        mediaQueryList.forEach((mql) => mql.removeEventListener(handler));
    },
    [] //          
  );

  return value;
}


useLocalStorage



Ce hook est conçu pour synchroniser l'état avec le stockage local pour conserver l'état lors des rechargements de page. L'utilisation de ce hook est similaire à l'utilisation de useState, sauf que nous transmettons la clé de stockage local par défaut lors du chargement de la page au lieu de définir une valeur initiale.



import { useState } from "react";

// 
function App() {
  //  useState,      ,    
  const [name, setName] = useLocalStorage("name", "Igor");

  return (
    <div>
      <input
        type="text"
        placeholder="Enter your name"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
    </div>
  );
}

// 
function useLocalStorage(key, initialValue) {
  //    
  //    useState   
  const [storedValue, setStoredValue] = useState(() => {
    try {
      //       
      const item = window.localStorage.getItem(key);
      //      initialValue
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      //   ,    
      console.error(error);
      return initialValue;
    }
  });

  //     useState,
  //       
  const setValue = (value) => {
    try {
      //    
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      //  
      setStoredValue(valueToStore);
      //     
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      //            
      console.error(error);
    }
  };

  return [storedValue, setValue];
}


C'est tout pour aujourd'hui. J'espère que vous avez trouvé quelque chose d'utile. Merci de votre attention.



All Articles