Frontend

React Hooks avanzados: patrones que uso en proyectos reales

React Hooks fueron una revolución cuando se introdujeron, simplificando la gestión del estado y los efectos secundarios. Pero más allá de los hooks básicos como useState y useEffect, hay patrones avanzados que nos permiten llevar nuestras aplicaciones React a un nuevo nivel. En este artículo, te voy a contar sobre algunos de los patrones avanzados de React Hooks que uso en proyectos reales como Menteo AI y RHINO. Vamos a meternos de lleno en custom hooks como useDebounce, useFetch, y useLocalStorage, veremos cuándo usar useMemo vs useCallback, exploraremos useReducer para manejar estados más complejos, y cómo componer hooks para lograr modularidad y reutilización.

Custom Hooks: useDebounce, useFetch, y useLocalStorage

Crear tus propios hooks personalizados es una de las mejores formas de reutilizar lógica en tus componentes. Empezando por useDebounce, este hook es útil cuando querés limitar la cantidad de veces que una función es ejecutada. Lo uso mucho en Menteo AI para optimizar las búsquedas en tiempo real.


// Hook que devuelve el valor pasado después de un retraso
import { useState, useEffect } from 'react';

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

export default useDebounce;

El useFetch es otro hook que uso frecuentemente, especialmente en proyectos como RHINO donde necesitamos manejar múltiples llamadas a APIs. Este hook simplifica la gestión del estado de la solicitud (cargando, éxito, error).


import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error('Error en la solicitud');
        const result = await response.json();
        setData(result);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

export default useFetch;

Finalmente, useLocalStorage es un hook que te permite sincronizar el estado con el localStorage del navegador. Es útil para mantener la persistencia de datos entre recargas, algo que implementamos en Merchant Hub Akua para guardar preferencias de usuario.


import { useState } from 'react';

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

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

export default useLocalStorage;

Cuándo usar useMemo vs useCallback

Uno de los dilemas comunes al trabajar con optimizaciones en React es decidir entre useMemo y useCallback. Aunque ambos se usan para memorizar valores, tienen usos distintos. En Menteo AI, por ejemplo, utilizamos useMemo para memorizar resultados de cálculos intensivos.

useMemo es ideal cuando tenés un cálculo costoso que no querés que se ejecute en cada renderizado. Imaginá que tenés una lista de usuarios que necesitás filtrar y ordenar.


import { useMemo } from 'react';

function UserList({ users }) {
  const sortedUsers = useMemo(() => {
    return users.sort((a, b) => a.name.localeCompare(b.name));
  }, [users]);

  return (
    <ul>
      {sortedUsers.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Por otro lado, useCallback es más útil cuando querés memorizar una función. Esto es particularmente importante para evitar que tus componentes hijos se vuelvan a renderizar innecesariamente.


import { useCallback } from 'react';

function ParentComponent() {
  const handleClick = useCallback(() => {
    console.log('Clickeado');
  }, []);

  return <ChildComponent onClick={handleClick} />;
}

function ChildComponent({ onClick }) {
  return <button onClick={onClick}>Click Me</button>;
}

En resumen, usá useMemo para memorizar resultados de funciones y useCallback para funciones en sí. Ambos son fundamentales para optimizar el rendimiento de tus aplicaciones.

useReducer para estado complejo

Cuando trabajás con estados complejos, useReducer se convierte en tu mejor aliado. En RHINO, lo usamos para manejar formularios complejos donde múltiples campos dependen entre sí.

Un ejemplo es un formulario de registro donde necesitás validar no solo los campos individuales sino también las interacciones entre ellos. Usá useReducer para gestionar este tipo de estado complejo.


import { useReducer } from 'react';

const initialState = {
  username: '',
  password: '',
  confirmPassword: '',
  valid: false,
};

function reducer(state, action) {
  switch (action.type) {
    case 'SET_USERNAME':
      return { ...state, username: action.payload };
    case 'SET_PASSWORD':
      return { ...state, password: action.payload };
    case 'SET_CONFIRM_PASSWORD':
      return { ...state, confirmPassword: action.payload };
    case 'VALIDATE':
      return { ...state, valid: state.password === state.confirmPassword };
    default:
      return state;
  }
}

function SignupForm() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const handleSubmit = (e) => {
    e.preventDefault();
    dispatch({ type: 'VALIDATE' });
    if (state.valid) {
      console.log('Formulario válido');
    } else {
      console.log('Error en la validación');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={state.username}
        onChange={(e) => dispatch({ type: 'SET_USERNAME', payload: e.target.value })}
      />
      <input
        type="password"
        value={state.password}
        onChange={(e) => dispatch({ type: 'SET_PASSWORD', payload: e.target.value })}
      />
      <input
        type="password"
        value={state.confirmPassword}
        onChange={(e) => dispatch({ type: 'SET_CONFIRM_PASSWORD', payload: e.target.value })}
      />
      <button type="submit">Registrarse</button>
    </form>
  );
}

useReducer es poderoso porque te permite manejar estados complejos de manera estructurada, similar a Redux pero más sencillo de configurar.

Composición de Hooks

La composición de hooks es un patrón avanzado que permite combinar múltiples hooks para crear lógica aún más poderosa y reutilizable. En Merchant Hub Akua, usamos esta técnica para encapsular lógica de autenticación y autorización.

Imaginá que necesitás un hook que maneje tanto la autenticación como el fetching de datos del usuario. Podés componer estos hooks para crear un useAuth que encapsule toda la lógica.


import useFetch from './useFetch';
import useLocalStorage from './useLocalStorage';

function useAuth() {
  const [token, setToken] = useLocalStorage('authToken', null);
  const { data: userData, loading, error } = useFetch('/api/user', token);

  const login = (newToken) => {
    setToken(newToken);
  };

  const logout = () => {
    setToken(null);
  };

  return { userData, loading, error, login, logout };
}

export default useAuth;

Con la composición de hooks, podés crear hooks más específicos y enfocados que se pueden reutilizar en diferentes partes de tu aplicación. Esto no solo mejora la organización de tu código, sino que también facilita la prueba y el mantenimiento.

Conclusión

Los patrones avanzados de React Hooks, como los custom hooks, el uso de useMemo y useCallback, useReducer para manejar estados complejos, y la composición de hooks, ofrecen herramientas poderosas para construir aplicaciones más eficientes y mantenibles. Estos patrones me han permitido optimizar aplicaciones en proyectos como Menteo AI, RHINO, y Merchant Hub Akua, mejorando tanto el rendimiento como la experiencia del desarrollador. Si querés discutir más sobre estos patrones o tenés alguna pregunta específica, no dudes en contactame.