¡Buen dia amigos!
Les presento a su atención los diez mejores ganchos personalizados .
Tabla de contenido
- useMemoCompare
- useAsync
- useRequireAuth
- useRouter
- useAuth
- useEventListener
- useWhyDidYouUpdate
- useDarkMode
- useMedia
- useLocalStorage
useMemoCompare
Este gancho es similar a useMemo, pero en lugar de una matriz de dependencias, se le pasa una función que compara los valores nuevos y anteriores. Una función puede comparar propiedades anidadas, llamar a métodos en objetos o hacer algo más con fines de comparación. Si la función devuelve verdadero, el gancho devuelve una referencia al objeto antiguo. Cabe señalar que este gancho, a diferencia de useMemo, no implica la ausencia de cálculos complejos repetidos. Necesita pasar el valor calculado para comparar. Esto puede resultar útil cuando desee compartir la biblioteca con otros desarrolladores y no desee obligarlos a recordar el objeto antes de enviarlo. Si se crea un objeto en el cuerpo de un componente (en el caso de que dependa de los accesorios), será nuevo en cada renderizado. Si el objeto es una dependencia de useEffect, el efecto se activará en cada renderizado,lo que puede dar lugar a problemas, hasta un bucle sin fin. Este gancho le permite evitar este desarrollo de eventos utilizando la referencia de objeto anterior en lugar de la nueva si la función reconoció los objetos como iguales.
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 una buena práctica mostrar el estado de una solicitud asincrónica. Un ejemplo
sería obtener datos de una API y mostrar un indicador de carga antes de mostrar los resultados. Otro ejemplo es deshabilitar el botón mientras se envía el formulario y luego mostrar el resultado. En lugar de contaminar el componente con muchas llamadas a useState para rastrear el estado de la función asíncrona, podemos usar este gancho, que toma una función asíncrona y devuelve valor, error y estado según sea necesario para actualizar la interfaz de usuario. Los valores posibles para la propiedad "estado" son "inactivo", "pendiente", "éxito" y "error". Nuestro gancho le permite ejecutar una función inmediatamente o tarde usando la función de ejecución.
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
El propósito de este gancho es redirigir al usuario a la página de inicio de sesión cuando se desconecta de la cuenta. Nuestro gancho es una composición de los ganchos "useAuth" y "useRouter". Por supuesto, podemos implementar la funcionalidad requerida en el gancho useAuth, pero luego tenemos que incluirlo con el esquema de enrutamiento. Con la composición, podemos mantener useAuth y useRouter simples implementando una redirección con un gancho personalizado.
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 está utilizando React Router en su trabajo, es posible que haya notado que recientemente han aparecido varios ganchos útiles, como "useParams", "useLocation", "useHistory" y "useRouterMatch". Intentemos envolverlos en un solo enlace que devuelva los datos y métodos que necesitamos. Le mostraremos cómo combinar múltiples ganchos y devolver un solo objeto que contiene sus estados. Para bibliotecas como React Router, tiene sentido proporcionar una selección del gancho deseado. Esto evita la renderización innecesaria. Pero a veces necesitamos todos o la mayoría de los ganchos con nombre.
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 común tener varios componentes que se renderizan dependiendo de si el usuario ha iniciado sesión en una cuenta. Algunos de estos componentes llaman a métodos de autenticación como inicio de sesión, cierre de sesión, sendPasswordResetEmail, etc. El gancho "useAuth" es perfecto para esto, ya que asegura que el componente recibe el estado de autenticación y vuelve a dibujar el componente cuando hay cambios. En lugar de instanciar useAuth para cada usuario, nuestro enlace llama a useContext para obtener datos del componente principal. La verdadera magia ocurre en el componente ProvideAuth, donde todos los métodos de autenticación (en el ejemplo, estamos usando Firebase) están envueltos en un gancho useProvideAuth. Luego, el contexto se usa para pasar el objeto de autenticación actual a los componentes secundarios que llaman a useAuth.Esto tendrá más sentido después de leer el ejemplo. Otra razón por la que me gusta este gancho es para abstraer el proveedor de autenticación real (Firebase), lo que facilita la realización de cambios.
// 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 tiene que lidiar con una gran cantidad de controladores de eventos que se registran con useEffect, es posible que desee separarlos en ganchos separados. En el siguiente ejemplo, creamos un gancho useEventListener que verifica la compatibilidad con addEventListener, agrega controladores y los elimina al salir.
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
Este gancho le permite determinar qué cambios de accesorios conducen a una nueva renderización. Si la función es "compleja" y está seguro de que está limpia, es decir devuelve los mismos resultados para los mismos accesorios, puede utilizar el componente de orden superior "React.memo" como lo hacemos en el ejemplo siguiente. Si después de eso los renderizados innecesarios no se han detenido, puede usar useWhyDidYouUpdate, que envía a la consola los accesorios que cambian durante el renderizado, indicando los valores anteriores y actuales.
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
Este gancho implementa la lógica para cambiar el esquema de color del sitio (claro y oscuro). Utiliza el almacenamiento local para almacenar el esquema seleccionado por el usuario, el modo predeterminado establecido en el navegador usando la consulta de medios "prefiere-esquema de color". Para habilitar el modo oscuro, use la clase "modo oscuro" del elemento "cuerpo". Hook también demuestra el poder de la composición. La sincronización de estado con localStorage se implementa usando el gancho "useLocalStorage" y definiendo el esquema preferido del usuario usando el gancho "useMedia", que están diseñados para diferentes propósitos. Sin embargo, componer estos ganchos da como resultado un gancho aún más poderoso de solo unas pocas líneas de código. Esto es casi lo mismo que el poder de "composición" de los ganchos en relación con el estado de los componentes.
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
Este gancho encapsula la lógica para definir consultas de medios. En el siguiente ejemplo, representamos un número diferente de columnas dependiendo de la consulta de medios según el ancho de pantalla actual y luego colocamos una imagen sobre las columnas para nivelar la diferencia en las alturas de las columnas (no queremos que una columna sea más alta que la otra) ... Puede crear un gancho que determine directamente el ancho de la pantalla, sin embargo, nuestro gancho le permite combinar consultas de medios especificadas en JS y una hoja de estilo.
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
Este enlace está diseñado para sincronizar el estado con el almacenamiento local para guardar el estado entre recargas de página. Usar este gancho es similar a usar useState, excepto que pasamos la clave de almacenamiento local como predeterminada en la carga de la página en lugar de definir un valor inicial.
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];
}
Eso es todo por hoy. Espero que hayas encontrado algo útil. Gracias por su atención.