React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

lodashutilitiesdebouncethrottletree-shakingperformance

Lodash - Battle-tested utility functions with tree-shakable imports for React apps

Recipe

# 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.

Working Example

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

  • Debounced search input preventing excessive filtering on every keystroke
  • groupBy to organize results by category
  • Proper cleanup of debounced function on component unmount
  • useMemo to create a stable debounced function reference

Deep Dive

How It Works

  • lodash-es provides ES module exports, enabling bundlers (webpack, Rollup, esbuild) to tree-shake unused functions
  • debounce delays function execution until a specified wait time has elapsed since the last call; cancel() prevents pending invocations
  • throttle limits execution to at most once per interval, useful for scroll/resize handlers
  • groupBy creates an object where keys come from the iteratee and values are arrays of matching elements
  • cloneDeep recursively copies objects, including nested objects, arrays, Maps, Sets, and Date instances
  • merge deep-merges objects, while Object.assign and spread only do shallow merging

Variations

Throttle 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");

TypeScript Notes

  • @types/lodash-es provides full type definitions for all functions
  • debounce and throttle return DebouncedFunc<T> with cancel() and flush() methods
  • groupBy returns Dictionary<T[]> where keys are strings
  • Most functions preserve input types; generics are inferred automatically
import type { DebouncedFunc } from "lodash-es";
 
// Explicitly typed debounced function
const debouncedSearch: DebouncedFunc<(query: string) => void> = debounce(
  (query: string) => {
    console.log("Searching:", query);
  },
  300
);

Gotchas

  • Importing all of lodashimport _ 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 targetmerge(target, source) mutates target. Fix: Pass an empty object as the first argument: merge({}, defaults, overrides).

Alternatives

FunctionNative AlternativeWhen to Use Lodash
cloneDeepstructuredClone()When cloning functions or class instances
getOptional chaining (?.)When path is a dynamic string variable
debounceNo native equivalentAlways (or use a small use-debounce package)
throttleNo native equivalentAlways
groupByObject.groupBy() (ES2024)When targeting older environments
mergeSpread {...a, ...b}When you need deep merge (spread is shallow)
uniqBy[...new Map(arr.map(x => [x.key, x])).values()]When readability matters
chunkNo native equivalentAlways

FAQs

Why should I use lodash-es instead of lodash?
  • lodash-es provides ES module exports that bundlers can tree-shake
  • import { debounce } from "lodash" bundles the entire library (~70KB)
  • import { debounce } from "lodash-es" includes only debounce and its dependencies
  • Always install @types/lodash-es alongside for TypeScript support
How do I correctly use debounce in a React component?
const 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.

Gotcha: Why does my debounced function fire on every keystroke?
  • Creating the debounced function inside the render body creates a new instance each render
  • Each new instance has its own timer, so the delay never accumulates
  • Fix: wrap with useMemo or useCallback so the same debounced function persists across renders
What is the difference between debounce and throttle?
  • debounce 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)
  • Both return functions with .cancel() and .flush() methods
Gotcha: Why does merge(target, source) modify my original object?
  • merge mutates the first argument (the target)
  • Always pass an empty object as the first argument: merge({}, defaults, overrides)
  • This creates a new object and leaves defaults and overrides unchanged
When should I use cloneDeep vs structuredClone?
  • structuredClone() is native and handles most types (objects, arrays, Maps, Sets, Dates)
  • cloneDeep also handles functions, RegExp with flags, and lodash-specific wrappers
  • Prefer structuredClone() for simple cases -- no dependency needed
  • Use cloneDeep when cloning objects that contain functions or class instances
Is _.get(obj, "a.b.c") still useful now that we have optional chaining?
  • For static paths, prefer obj?.a?.b?.c -- it is native and type-safe
  • get 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")
How do I type a debounced function in TypeScript?
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.

How does groupBy work and what type does it return in 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: [...] }

Returns Dictionary<T[]> where keys are strings.

How do I safely remove keys from an object without mutation?
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.

How do I split an array into fixed-size chunks?
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.

How do I prevent memory leaks from debounced/throttled functions in React?
  • Pending debounced calls fire after the component unmounts if not cancelled
  • Call .cancel() in the useEffect cleanup function
  • This prevents state updates on unmounted components
  • Applies to both debounce and throttle
  • React Hooks — Using debounce/throttle in custom hooks
  • React Patterns — Performance patterns with utility functions
  • TanStack Query — Built-in deduplication reduces need for debounce in data fetching