Benutzerdefinierte Haken. Teil 1





Guten Tag, Freunde!



Ich präsentiere Ihnen die zehn besten benutzerdefinierten Haken .



Inhaltsverzeichnis







useMemoCompare



Dieser Hook ähnelt useMemo, es wird jedoch anstelle eines Arrays von Abhängigkeiten eine Funktion übergeben, die die vorherigen und neuen Werte vergleicht. Eine Funktion kann verschachtelte Eigenschaften vergleichen, Methoden für Objekte aufrufen oder zu Vergleichszwecken etwas anderes tun. Wenn die Funktion true zurückgibt, gibt der Hook einen Verweis auf das alte Objekt zurück. Es ist zu beachten, dass dieser Hook im Gegensatz zu useMemo nicht das Fehlen wiederholter komplexer Berechnungen impliziert. Er muss den berechneten Wert zum Vergleich übergeben. Dies kann nützlich sein, wenn Sie die Bibliothek für andere Entwickler freigeben und sie nicht zwingen möchten, sich vor dem Senden an das Objekt zu erinnern. Wenn ein Objekt im Hauptteil einer Komponente erstellt wird (falls es von Requisiten abhängt), ist es bei jedem Rendern neu. Wenn das Objekt eine Abhängigkeit von useEffect ist, wird der Effekt bei jedem Rendern ausgelöst.was zu Problemen führen kann, bis zu einer Endlosschleife. Mit diesem Hook können Sie diese Entwicklung von Ereignissen vermeiden, indem Sie die alte Objektreferenz anstelle der neuen verwenden, wenn die Funktion die Objekte als dieselben erkennt.



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



Es wird empfohlen, den Status einer asynchronen Anforderung anzuzeigen. Ein Beispiel

wäre das Abrufen von Daten von einer API und das Anzeigen eines Ladeindikators vor dem Rendern der Ergebnisse. Ein weiteres Beispiel ist das Deaktivieren der Schaltfläche, während das Formular gesendet wird, und das Anzeigen des Ergebnisses. Anstatt die Komponente mit vielen useState-Aufrufen zu verschmutzen, um den Status der asynchronen Funktion zu verfolgen, können wir diesen Hook verwenden, der eine asynchrone Funktion verwendet und die Werte "value", "error" und "status" zurückgibt, die zum Aktualisieren der Benutzeroberfläche erforderlich sind. Mögliche Werte für die Eigenschaft "Status" sind "Leerlauf", "Ausstehend", "Erfolg" und "Fehler". Mit unserem Hook können Sie eine Funktion sofort oder spät mit der Funktion execute ausführen.



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



Der Zweck dieses Hooks besteht darin, den Benutzer beim Abmelden vom Konto auf die Anmeldeseite umzuleiten. Unser Hook ist eine Komposition aus den Hooks "useAuth" und "useRouter". Natürlich können wir die erforderliche Funktionalität in den useAuth-Hook implementieren, aber dann müssen wir sie in das Routing-Schema aufnehmen. Mit der Komposition können wir useAuth und useRouter einfach halten, indem wir eine Umleitung mit einem benutzerdefinierten Hook implementieren.



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



Wenn Sie React Router in Ihrer Arbeit verwenden, haben Sie möglicherweise festgestellt, dass kürzlich mehrere nützliche Hooks aufgetreten sind, z. B. "useParams", "useLocation", "useHistory" und "useRouterMatch". Versuchen wir, sie in einem einzigen Hook zusammenzufassen, der die benötigten Daten und Methoden zurückgibt. Wir zeigen Ihnen, wie Sie mehrere Hooks kombinieren und ein einzelnes Objekt mit ihren Status zurückgeben. Für Bibliotheken wie React Router ist es sinnvoll, eine Auswahl des gewünschten Hooks bereitzustellen. Dies vermeidet unnötiges Rendern. Aber manchmal brauchen wir alle oder die meisten der genannten Haken.



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



Es ist üblich, dass mehrere Komponenten gerendert werden, je nachdem, ob der Benutzer in einem Konto angemeldet ist. Einige dieser Komponenten rufen Authentifizierungsmethoden wie Anmelden, Abmelden, sendPasswordResetEmail usw. auf. Der "useAuth" -Hook ist dafür perfekt geeignet, um sicherzustellen, dass die Komponente den Authentifizierungsstatus erhält und die Komponente neu zeichnet, wenn Änderungen vorliegen. Anstatt useAuth für jeden Benutzer zu instanziieren, ruft unser Hook useContext auf, um Daten von der übergeordneten Komponente abzurufen. Die wahre Magie findet in der Komponente "ProvideAuth" statt, in der alle Authentifizierungsmethoden (in dem Beispiel, in dem wir Firebase verwenden) in einen "useProvideAuth" -Hook eingeschlossen sind. Der Kontext wird dann verwendet, um das aktuelle Authentifizierungsobjekt an die untergeordneten Komponenten zu übergeben, die useAuth aufrufen.Dies ist nach dem Lesen des Beispiels sinnvoller. Ein weiterer Grund, warum ich diesen Hook mag, ist, dass er den echten Authentifizierungsanbieter (Firebase) abstrahiert, was das Vornehmen von Änderungen erleichtert.



//   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



Wenn Sie mit einer großen Anzahl von Ereignishandlern zu tun haben, die sich bei useEffect registrieren, möchten Sie diese möglicherweise in separate Hooks aufteilen. Im folgenden Beispiel erstellen wir einen useEventListener-Hook, der nach der Unterstützung von addEventListener sucht, Handler hinzufügt und diese beim Beenden entfernt.

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



Mit diesem Hook können Sie bestimmen, welche Requisitenänderungen zum erneuten Rendern führen. Wenn die Funktion "komplex" ist und Sie sicher sind, dass sie sauber ist, d. H. Gibt die gleichen Ergebnisse für die gleichen Requisiten zurück. Sie können die Komponente "React.memo" höherer Ordnung wie im folgenden Beispiel verwenden. Wenn danach unnötige Rendervorgänge nicht gestoppt wurden, können Sie useWhyDidYouUpdate verwenden, das die Requisiten, die sich während des Renderns ändern, an die Konsole ausgibt und die vorherigen und aktuellen Werte angibt.



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



Dieser Hook implementiert die Logik zum Umschalten des Farbschemas der Site (hell und dunkel). Es verwendet lokalen Speicher zum Speichern des vom Benutzer ausgewählten Schemas, dem Standardmodus, der im Browser mithilfe der Medienabfrage "Bevorzugtes Farbschema" festgelegt wurde. Verwenden Sie zum Aktivieren des Dunkelmodus die Klasse "Dunkelmodus" des Elements "Körper". Hook demonstriert auch die Kraft der Komposition. Die Statussynchronisation mit localStorage wird mithilfe des Hooks "useLocalStorage" implementiert und das bevorzugte Schema des Benutzers mithilfe des Hooks "useMedia" definiert, der für verschiedene Zwecke entwickelt wurde. Das Zusammenstellen dieser Hooks führt jedoch zu einem noch leistungsstärkeren Hook mit nur wenigen Codezeilen. Dies entspricht fast der "Zusammensetzungskraft" von Haken in Bezug auf den Zustand der Komponenten.



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



Dieser Hook kapselt die Logik zum Definieren von Medienabfragen. Im folgenden Beispiel rendern wir abhängig von der Medienabfrage basierend auf der aktuellen Bildschirmbreite eine unterschiedliche Anzahl von Spalten und platzieren dann ein Bild über den Spalten, damit der Unterschied in den Spaltenhöhen ausgeglichen wird (wir möchten nicht, dass eine Spalte höher als die andere ist). ... Sie können einen Hook erstellen, der direkt die Breite des Bildschirms bestimmt. Mit unserem Hook können Sie jedoch die in JS angegebenen Medienabfragen mit einem Stylesheet kombinieren.



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



Dieser Hook dient zum Synchronisieren des Status mit dem lokalen Speicher, um den Status beim erneuten Laden der Seite beizubehalten. Die Verwendung dieses Hooks ähnelt der Verwendung von useState, außer dass wir den lokalen Speicherschlüssel als Standard beim Laden der Seite übergeben, anstatt einen Anfangswert zu definieren.



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];
}


Das ist alles für heute. Hoffe du hast etwas hilfreiches gefunden. Danke für die Aufmerksamkeit.



All Articles