Lodash - Funciones utilitarias probadas en batalla con importaciones que pueden hacer tree-shaking para aplicaciones React
Receta
# Versión de módulo ES que puede hacer tree-shaking (recomendado)
npm install lodash-es
npm install -D @types/lodash-es
# O versión clásica con importaciones seleccionadas
npm install lodash
npm install -D @types/lodash// Importa funciones individuales para tree-shaking
import debounce from "lodash-es/debounce";
import groupBy from "lodash-es/groupBy";
import cloneDeep from "lodash-es/cloneDeep";
// O importaciones nombradas (funciona con lodash-es)
import { debounce, groupBy, cloneDeep } from "lodash-es";Cuándo usarlo: Necesitas funciones utilitarias confiables para debouncing, throttling, clonación profunda, agrupación o fusión que manejen casos extremos mejor que soluciones rápidas caseras.
Ejemplo Funcional
// app/components/SearchWithDebounce.tsx
"use client";
import { useState, useCallback, useMemo, useEffect } from "react";
import debounce from "lodash-es/debounce";
import groupBy from "lodash-es/groupBy";
interface Product {
id: number;
name: string;
category: string;
price: number;
}
const ALL_PRODUCTS: Product[] = [
{ id: 1, name: "React Handbook", category: "books", price: 29 },
{ id: 2, name: "TypeScript Guide", category: "books", price: 35 },
{ id: 3, name: "Mechanical Keyboard", category: "electronics", price: 150 },
{ id: 4, name: "USB-C Hub", category: "electronics", price: 45 },
{ id: 5, name: "Standing Desk", category: "furniture", price: 400 },
{ id: 6, name: "Monitor Arm", category: "furniture", price: 80 },
];
export default function SearchWithDebounce() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<Product[]>(ALL_PRODUCTS);
const searchProducts = useMemo(
() =>
debounce((searchQuery: string) => {
const filtered = ALL_PRODUCTS.filter((p) =>
p.name.toLowerCase().includes(searchQuery.toLowerCase())
);
setResults(filtered);
}, 300),
[]
);
// Limpieza de debounce al desmontar
useEffect(() => {
return () => {
searchProducts.cancel();
};
}, [searchProducts]);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
searchProducts(e.target.value);
},
[searchProducts]
);
const grouped = groupBy(results, "category");
return (
<div className="max-w-lg mx-auto p-6">
<input
value={query}
onChange={handleChange}
placeholder="Buscar productos..."
className="w-full border rounded px-3 py-2 mb-4"
/>
{Object.entries(grouped).map(([category, items]) => (
<div key={category} className="mb-4">
<h3 className="font-bold capitalize text-lg">{category}</h3>
<ul className="mt-1 space-y-1">
{items.map((item) => (
<li key={item.id} className="flex justify-between">
<span>{item.name}</span>
<span className="text-gray-500">${item.price}</span>
</li>
))}
</ul>
</div>
))}
<p className="text-sm text-gray-400 mt-4">
{results.length} resultados encontrados
</p>
</div>
);
}Lo que demuestra esto:
- Búsqueda con debounce que previene filtrado excesivo en cada pulsación de tecla
groupBypara organizar resultados por categoría- Limpieza apropiada de la función debounced al desmontar el componente
useMemopara crear una referencia estable a la función debounced
Análisis Profundo
Cómo Funciona
lodash-esproporciona exportaciones de módulos ES, permitiendo que los bundlers (webpack, Rollup, esbuild) hagan tree-shaking de funciones no utilizadasdebounceretrasa la ejecución de funciones hasta que ha transcurrido un tiempo de espera específico desde la última llamada;cancel()previene invocaciones pendientesthrottlelimita la ejecución a como máximo una vez por intervalo, útil para manejadores de scroll/resizegroupBycrea un objeto donde las claves provienen del iteratee y los valores son arrays de elementos coincidentescloneDeepcopia recursivamente objetos, incluyendo objetos anidados, arrays, Maps, Sets e instancias de Datemergefusiona objetos profundamente, mientras queObject.assigny spread solo hacen fusión superficial
Variaciones
Throttle para manejadores de scroll/resize:
"use client";
import { useEffect, useState } from "react";
import throttle from "lodash-es/throttle";
export function useScrollPosition() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = throttle(() => {
setScrollY(window.scrollY);
}, 100);
window.addEventListener("scroll", handleScroll);
return () => {
handleScroll.cancel();
window.removeEventListener("scroll", handleScroll);
};
}, []);
return scrollY;
}Fusión profunda de objetos de configuración:
import merge from "lodash-es/merge";
const defaultConfig = {
theme: { colors: { primary: "#3b82f6", secondary: "#64748b" } },
features: { darkMode: false, notifications: true },
};
const userConfig = {
theme: { colors: { primary: "#ef4444" } },
features: { darkMode: true },
};
const config = merge({}, defaultConfig, userConfig);
// { theme: { colors: { primary: "#ef4444", secondary: "#64748b" } },
// features: { darkMode: true, notifications: true } }Clonación profunda segura:
import cloneDeep from "lodash-es/cloneDeep";
const original = {
nested: { value: 42, date: new Date(), set: new Set([1, 2, 3]) },
};
const copy = cloneDeep(original);
copy.nested.value = 100;
console.log(original.nested.value); // 42 (sin cambios)Otras utilidades comunes:
import pick from "lodash-es/pick";
import omit from "lodash-es/omit";
import uniqBy from "lodash-es/uniqBy";
import chunk from "lodash-es/chunk";
import get from "lodash-es/get";
// Selecciona claves específicas de un objeto
const user = { id: 1, name: "Alice", email: "alice@example.com", password: "secret" };
const safe = pick(user, ["id", "name", "email"]);
// Elimina claves
const noPassword = omit(user, ["password"]);
// Deduplica por una propiedad
const unique = uniqBy(users, "email");
// Divide array en páginas
const pages = chunk(items, 10); // [[...10], [...10], ...]
// Acceso seguro a propiedades profundas (considera optional chaining en su lugar)
const value = get(config, "deeply.nested.value", "default");Notas de TypeScript
@types/lodash-esproporciona definiciones de tipos completas para todas las funcionesdebounceythrottledevuelvenDebouncedFunc<T>con métodoscancel()yflush()groupBydevuelveDictionary<T[]>donde las claves son strings- La mayoría de funciones preservan tipos de entrada; los genéricos se infieren automáticamente
import type { DebouncedFunc } from "lodash-es";
// Función debounced explícitamente tipada
const debouncedSearch: DebouncedFunc<(query: string) => void> = debounce(
(query: string) => {
console.log("Buscando:", query);
},
300
);Trampas
-
Importar todo de lodash —
import _ from "lodash"agrupa toda la librería (alrededor de 70KB minificado). Solución: Usalodash-escon importaciones nombradas o importa rutas específicas comolodash-es/debounce. -
Debounce en render — Crear una función debounced dentro de render crea una nueva instancia cada render, frustrando el propósito. Solución: Envuelve con
useMemoouseCallbacky proporciona una referencia estable. -
Memory leaks de debounce/throttle — Las llamadas debounced pendientes pueden ejecutarse después del desmontaje del componente. Solución: Llama
.cancel()en la función de limpieza deuseEffect. -
cloneDeep es costoso — Clonar profundamente objetos grandes es lento. Solución: Usa
structuredClone()(nativo, disponible en todos los navegadores modernos y Node 17+) para casos simples. UsacloneDeepsolo cuando necesites manejar funciones o características especiales de Lodash. -
get vs optional chaining —
_.get(obj, "a.b.c")es redundante ahora que JavaScript tieneobj?.a?.b?.c. Solución: Prefiere optional chaining para acceso a propiedades. Usagetsolo cuando la ruta es dinámica (una variable). -
merge mutates el objetivo —
merge(target, source)mutatarget. Solución: Pasa un objeto vacío como primer argumento:merge({}, defaults, overrides).
Alternativas
| Función | Alternativa Nativa | Cuándo Usar Lodash |
|---|---|---|
cloneDeep | structuredClone() | Cuando clonas funciones o instancias de clase |
get | Optional chaining (?.) | Cuando la ruta es una variable de cadena dinámica |
debounce | Sin equivalente nativo | Siempre (o usa un pequeño paquete use-debounce) |
throttle | Sin equivalente nativo | Siempre |
groupBy | Object.groupBy() (ES2024) | Cuando te diriges a entornos antiguos |
merge | Spread {...a, ...b} | Cuando necesitas fusión profunda (spread es superficial) |
uniqBy | [...new Map(arr.map(x => [x.key, x])).values()] | Cuando la legibilidad importa |
chunk | Sin equivalente nativo | Siempre |
Preguntas Frecuentes
¿Por qué debería usar lodash-es en lugar de lodash?
lodash-esproporciona exportaciones de módulos ES que los bundlers pueden hacer tree-shakeimport { debounce } from "lodash"agrupa toda la librería (~70KB)import { debounce } from "lodash-es"incluye solodebouncey sus dependencias- Siempre instala
@types/lodash-esjunto a lodash para soporte de TypeScript
¿Cómo uso correctamente debounce en un componente React?
const searchFn = useMemo(
() => debounce((query: string) => {
// realizar búsqueda
}, 300),
[]
);
useEffect(() => {
return () => searchFn.cancel();
}, [searchFn]);Envuelve en useMemo para una referencia estable. Limpia con .cancel() al desmontar.
Trampa: ¿Por qué mi función debounced se ejecuta en cada pulsación de tecla?
- Crear la función debounced dentro del cuerpo de render crea una nueva instancia cada render
- Cada nueva instancia tiene su propio temporizador, así que el retraso nunca se acumula
- Solución: envuelve con
useMemoouseCallbackpara que la misma función debounced persista entre renders
¿Cuál es la diferencia entre debounce y throttle?
debounceespera N ms de inactividad antes de ejecutar (bueno para búsquedas de entrada)throttlese ejecuta como máximo una vez por intervalo de N ms (bueno para manejadores de scroll/resize)- Ambos devuelven funciones con métodos
.cancel()y.flush()
Trampa: ¿Por qué merge(target, source) modifica mi objeto original?
mergemutates el primer argumento (el objetivo)- Siempre pasa un objeto vacío como primer argumento:
merge({}, defaults, overrides) - Esto crea un nuevo objeto y deja
defaultsyoverridessin cambios
¿Cuándo debería usar cloneDeep vs structuredClone?
structuredClone()es nativo y maneja la mayoría de tipos (objetos, arrays, Maps, Sets, Dates)cloneDeeptambién maneja funciones, RegExp con banderas y envolturas específicas de lodash- Prefiere
structuredClone()para casos simples -- no se necesita dependencia - Usa
cloneDeepcuando clonas objetos que contienen funciones o instancias de clase
¿Sigue siendo útil _.get(obj, "a.b.c") ahora que tenemos optional chaining?
- Para rutas estáticas, prefiere
obj?.a?.b?.c-- es nativo y type-safe getsigue siendo útil cuando la ruta es una variable dinámica:get(obj, dynamicPath, defaultValue)gettambién soporta notación de array en rutas:get(obj, "items[0].name")
¿Cómo escribo una función debounced en TypeScript?
import type { DebouncedFunc } from "lodash-es";
const debouncedSearch: DebouncedFunc<(q: string) => void> =
debounce((q: string) => {
console.log("Buscando:", q);
}, 300);DebouncedFunc<T> agrega .cancel() y .flush() al tipo de función envuelta.
¿Cómo funciona groupBy y qué tipo devuelve en TypeScript?
import groupBy from "lodash-es/groupBy";
const products = [
{ name: "Book", category: "media" },
{ name: "CD", category: "media" },
{ name: "Desk", category: "furniture" },
];
const grouped = groupBy(products, "category");
// { media: [...], furniture: [...] }Devuelve Dictionary<T[]> donde las claves son strings.
¿Cómo elimino claves de un objeto de forma segura sin mutación?
import omit from "lodash-es/omit";
const user = { id: 1, name: "Alice", password: "secret" };
const safe = omit(user, ["password"]);
// { id: 1, name: "Alice" }omit devuelve un nuevo objeto; el original no se modifica.
¿Cómo divido un array en trozos de tamaño fijo?
import chunk from "lodash-es/chunk";
const items = [1, 2, 3, 4, 5, 6, 7];
const pages = chunk(items, 3);
// [[1, 2, 3], [4, 5, 6], [7]]Útil para paginación o procesamiento por lotes de solicitudes API.
¿Cómo prevengo memory leaks de funciones debounced/throttled en React?
- Las llamadas debounced pendientes se ejecutan después del desmontaje del componente si no se cancelan
- Llama
.cancel()en la función de limpieza deuseEffect - Esto previene actualizaciones de estado en componentes desmontados
- Se aplica tanto a
debouncecomo athrottle
Relacionado
- React Hooks — Usar debounce/throttle en hooks personalizados
- React Patterns — Patrones de rendimiento con funciones utilitarias
- TanStack Query — La deduplicación incorporada reduce la necesidad de debounce en obtención de datos