Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
# Tree-shakable ES module version (recommended)
npm install lodash-es
npm install -D @types/lodash-es
# Or classic version with cherry-picked imports
npm install lodash
npm install -D @types/lodash// Import individual functions for tree-shaking
import debounce from "lodash-es/debounce";
import groupBy from "lodash-es/groupBy";
import cloneDeep from "lodash-es/cloneDeep";
// Or named imports (works with lodash-es)
import { debounce, groupBy, cloneDeep } from "lodash-es";When to reach for this: You need reliable utility functions for debouncing, throttling, deep cloning, grouping, or merging that handle edge cases better than quick hand-rolled solutions.
// 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),
[]
);
// Cleanup debounce on unmount
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="Search products..."
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} results found
</p>
</div>
);
}What this demonstrates:
groupBy to organize results by categoryuseMemo to create a stable debounced function referencelodash-es provides ES module exports, enabling bundlers (webpack, Rollup, esbuild) to tree-shake unused functionsdebounce delays function execution until a specified wait time has elapsed since the last call; cancel() prevents pending invocationsthrottle limits execution to at most once per interval, useful for scroll/resize handlersgroupBy creates an object where keys come from the iteratee and values are arrays of matching elementscloneDeep recursively copies objects, including nested objects, arrays, Maps, Sets, and Date instancesmerge deep-merges objects, while Object.assign and spread only do shallow mergingThrottle for scroll/resize handlers:
"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;
}Deep merge configuration objects:
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 } }Safe deep cloning:
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 (unchanged)Other common utilities:
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";
// Pick specific keys from an object
const user = { id: 1, name: "Alice", email: "alice@example.com", password: "secret" };
const safe = pick(user, ["id", "name", "email"]);
// Remove keys
const noPassword = omit(user, ["password"]);
// Deduplicate by a property
const unique = uniqBy(users, "email");
// Split array into pages
const pages = chunk(items, 10); // [[...10], [...10], ...]
// Safe deep property access (consider optional chaining instead)
const value = get(config, "deeply.nested.value", "default");@types/lodash-es provides full type definitions for all functionsdebounce and throttle return DebouncedFunc<T> with cancel() and flush() methodsgroupBy returns Dictionary<T[]> where keys are stringsimport type { DebouncedFunc } from "lodash-es";
// Explicitly typed debounced function
const debouncedSearch: DebouncedFunc<(query: string) => void> = debounce(
(query: string) => {
console.log("Searching:", query);
},
300
);Importing all of lodash - import _ from "lodash" bundles the entire library (around 70KB minified). Fix: Use lodash-es with named imports or import specific paths like lodash-es/debounce.
Debounce in render - Creating a debounced function inside render creates a new instance every render, defeating the purpose. Fix: Wrap with useMemo or useCallback and provide a stable reference.
Memory leaks from debounce/throttle - Pending debounced calls can fire after component unmount. Fix: Call .cancel() in the cleanup function of useEffect.
cloneDeep is expensive - Deep cloning large objects is slow. Fix: Use structuredClone() (native, available in all modern browsers and Node 17+) for simple cases. Use cloneDeep only when you need to handle functions or special Lodash features.
get vs optional chaining - _.get(obj, "a.b.c") is redundant now that JavaScript has obj?.a?.b?.c. Fix: Prefer optional chaining for property access. Use get only when the path is dynamic (a variable).
merge mutates the target - merge(target, source) mutates target. Fix: Pass an empty object as the first argument: merge({}, defaults, overrides).
| Function | Native Alternative | When to Use Lodash |
|---|---|---|
cloneDeep | structuredClone() | When cloning functions or class instances |
get | Optional chaining (?.) | When path is a dynamic string variable |
debounce | No native equivalent | Always (or use a small use-debounce package) |
throttle | No native equivalent | Always |
groupBy | Object.groupBy() (ES2024) | When targeting older environments |
merge | Spread {...a, ...b} | When you need deep merge (spread is shallow) |
uniqBy | [...new Map(arr.map(x => [x.key, x])).values()] | When readability matters |
chunk | No native equivalent | Always |
lodash-es provides ES module exports that bundlers can tree-shakeimport { debounce } from "lodash" bundles the entire library (~70KB)import { debounce } from "lodash-es" includes only debounce and its dependencies@types/lodash-es alongside for TypeScript supportconst searchFn = useMemo(
() => debounce((query: string) => {
// perform search
}, 300),
[]
);
useEffect(() => {
return () => searchFn.cancel();
}, [searchFn]);Wrap in useMemo for a stable reference. Clean up with .cancel() on unmount.
useMemo or useCallback so the same debounced function persists across rendersdebounce waits until N ms of inactivity before executing (good for search inputs)throttle executes at most once per N ms interval (good for scroll/resize handlers).cancel() and .flush() methodsmerge mutates the first argument (the target)merge({}, defaults, overrides)defaults and overrides unchangedstructuredClone() is native and handles most types (objects, arrays, Maps, Sets, Dates)cloneDeep also handles functions, RegExp with flags, and lodash-specific wrappersstructuredClone() for simple cases -- no dependency neededcloneDeep when cloning objects that contain functions or class instancesobj?.a?.b?.c -- it is native and type-safeget is still useful when the path is a dynamic variable: get(obj, dynamicPath, defaultValue)get also supports array notation in paths: get(obj, "items[0].name")import type { DebouncedFunc } from "lodash-es";
const debouncedSearch: DebouncedFunc<(q: string) => void> =
debounce((q: string) => {
console.log("Searching:", q);
}, 300);DebouncedFunc<T> adds .cancel() and .flush() to the wrapped function type.
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: [...] }Returns Dictionary<T[]> where keys are strings.
import omit from "lodash-es/omit";
const user = { id: 1, name: "Alice", password: "secret" };
const safe = omit(user, ["password"]);
// { id: 1, name: "Alice" }omit returns a new object; the original is not modified.
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]]Useful for pagination or batching API requests.
.cancel() in the useEffect cleanup functiondebounce and throttleReviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥