You Don’t Need a Library for That: 12 Things Developers Still Install npm Packages For in 2026

Deep cloning, UUID generation, date formatting, debouncing, flattening arrays, generating ranges, shuffling, truncating strings — twelve things native JavaScript and the Web Platform have handled for years while your package.json keeps growing.


The reflex is understandable. You need to debounce a function. You search npm. You install lodash. You use one function. You’ve added 70KB to your bundle.

Most of the time, the thing you’re about to install a package for is something the language or the platform already handles — either natively or in five lines of code that are more readable, more performant, and more maintainable than the package you were about to add.

This is twelve of them. Each one is a package people still reach for in 2026 that has a native or near-native replacement ready to use today.


1. UUID Generation → crypto.randomUUID()

Still installing: uuid, nanoid, shortid

// ✗ What people install
import { v4 as uuidv4 } from 'uuid'
const id = uuidv4()

// ✓ Native — every modern browser and Node.js 14.17+
const id = crypto.randomUUID()
// → 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

crypto.randomUUID() is cryptographically secure, faster than any package (it uses the platform’s native RNG), and produces spec-compliant UUID v4 strings. There is no reason to install uuid for standard ID generation in 2026.

Keep the package if: You need UUID versions other than v4 (v1 time-based, v3/v5 name-based).


2. Deep Cloning → structuredClone()

Still installing: lodash (for _.cloneDeep), clone, rfdc

// ✗ What people install
import { cloneDeep } from 'lodash-es'
const copy = cloneDeep(complexObject)

// ✓ Native — Baseline Widely Available since 2022
const copy = structuredClone(complexObject)

structuredClone() handles: nested objects and arrays, Date, Map, Set, ArrayBuffer, RegExp, circular references (yes, circular references), typed arrays, and more. It’s implemented at the engine level and faster than any JavaScript deep-clone implementation.

// It handles things lodash cloneDeep doesn't
const original = { date: new Date(), map: new Map([['key', 'value']]) }
const clone = structuredClone(original)

clone.date instanceof Date  // ✓ true — Date is preserved, not stringified
clone.map instanceof Map    // ✓ true — Map is preserved
original.map === clone.map  // false — deep copy, not reference

Keep the package if: You need to clone class instances with prototype methods preserved. structuredClone() strips prototype methods — you get the data, not the class.


3. Date Formatting → Intl.DateTimeFormat

Still installing: date-fns, dayjs, moment

// ✗ What people install
import { format } from 'date-fns'
format(new Date(), 'MMM d, yyyy')

// ✓ Native — localisation-aware, zero bundle cost
new Intl.DateTimeFormat('en-IN', {
  year:  'numeric',
  month: 'short',
  day:   'numeric',
}).format(new Date())
// → 'May 25, 2026'

// For relative time ('3 days ago', '2 hours from now')
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
rtf.format(-3, 'day')   // → '3 days ago'
rtf.format(2,  'hour')  // → 'in 2 hours'

Bonus: Intl.DateTimeFormat is locale-aware by default. Pass the user’s locale and you get localised output without any extra configuration — something that requires significant setup in date libraries.

Keep the package if: You need date arithmetic (addDays, startOfWeek), complex parsing with custom formats, or full timezone support. Intl formats and computes relative time; it doesn’t do arithmetic.


4. Debounce → 8 Lines

Still installing: lodash, lodash-es, debounce, throttle-debounce

// ✗ Importing 70KB+ for one function
import debounce from 'lodash/debounce'

// ✓ The whole implementation
function debounce(fn, delay) {
  let timer
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// With cancel support
function debounce(fn, delay) {
  let timer
  const debounced = function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
  debounced.cancel = () => clearTimeout(timer)
  return debounced
}

Debounce is eight lines of code. It has been eight lines of code since 2010. If you’re installing a 70KB library for this, you are paying a very large price for eight lines.


5. Array Flattening → .flat() and .flatMap()

Still installing: lodash (for _.flatten, _.flattenDeep), array-flatten

// ✗ What people install
import { flatten, flattenDeep } from 'lodash-es'
flatten([1, [2, [3, [4]]]])       // → [1, 2, [3, [4]]]
flattenDeep([1, [2, [3, [4]]]])   // → [1, 2, 3, 4]

// ✓ Native — ES2019, all modern browsers
[1, [2, [3, [4]]]].flat()         // → [1, 2, [3, [4]]] (one level)
[1, [2, [3, [4]]]].flat(Infinity) // → [1, 2, 3, 4] (all levels)
[1, [2, [3, [4]]]].flat(2)        // → [1, 2, 3, [4]] (two levels)

// .flatMap() maps and flattens in one pass — more efficient than separate map + flat
const sentences = ['Hello World', 'Foo Bar']
sentences.flatMap(s => s.split(' '))
// → ['Hello', 'World', 'Foo', 'Bar']

6. Grouping Arrays → Object.groupBy()

Still installing: lodash (for _.groupBy)

// ✗ What people install
import { groupBy } from 'lodash-es'
groupBy(orders, 'status')

// ✓ Native — ES2024, Baseline Widely Available 2024
const grouped = Object.groupBy(orders, order => order.status)
// → { pending: [...], shipped: [...], delivered: [...] }

// Also: Map.groupBy() for Map output (keys can be objects, not just strings)
const byDate = Map.groupBy(events, event =>
  new Date(event.date).toDateString()
)

7. Conditional Class Names → Template Literal + Filter

Still installing: classnames, clsx

// ✗ What people install (0.5KB — barely worth it)
import cn from 'classnames'
cn('btn', { 'btn--primary': isPrimary, 'btn--disabled': isDisabled })

// ✓ Native — readable and zero dependency
const classes = [
  'btn',
  isPrimary  && 'btn--primary',
  isDisabled && 'btn--disabled',
  size === 'large' && 'btn--large',
].filter(Boolean).join(' ')

// Or as a utility (3 lines):
const cn = (...args) =>
  args.flatMap(a => typeof a === 'string' ? a
    : Object.entries(a).filter(([, v]) => v).map(([k]) => k))
  .filter(Boolean).join(' ')

8. Generating Ranges → Array.from()

Still installing: lodash (for _.range), range

// ✗ What people install
import { range } from 'lodash-es'
range(1, 11)         // → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
range(0, 50, 10)     // → [0, 10, 20, 30, 40]

// ✓ Native — Array.from() with a map function
Array.from({ length: 10 }, (_, i) => i + 1)    // → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Array.from({ length: 5  }, (_, i) => i * 10)   // → [0, 10, 20, 30, 40]
Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i)) // → ['A', 'B', ..., 'Z']

// Utility if you want lodash's exact API:
const range = (start, end, step = 1) =>
  Array.from({ length: Math.ceil((end - start) / step) }, (_, i) => start + i * step)

9. Unique Values → Set

Still installing: lodash (for _.uniq, _.uniqBy)

// ✗ What people install
import { uniq, uniqBy } from 'lodash-es'
uniq([1, 1, 2, 3, 3])
uniqBy(users, 'id')

// ✓ Native — Set for primitives
const unique = [...new Set([1, 1, 2, 3, 3])]  // → [1, 2, 3]

// For objects — Map keyed by a property (uniqBy equivalent)
const uniqueById = [...new Map(users.map(u => [u.id, u])).values()]
// Preserves the last occurrence; reverse first to preserve first occurrence

10. Truncating Text → Intl.Segmenter + CSS

Still installing: truncate, text-truncate, lodash’s _.truncate

// ✗ What people install
import { truncate } from 'lodash-es'
truncate('The quick brown fox', { length: 15 })
// → 'The quick br...'

// ✓ Native — handles Unicode correctly
function truncate(str, maxLength, suffix = '…') {
  if (str.length <= maxLength) return str
  return str.slice(0, maxLength - suffix.length) + suffix
}

// ✓ Even better — truncate at word boundaries using Intl.Segmenter
function truncateWords(str, maxLength, suffix = '…') {
  if (str.length <= maxLength) return str

  const segmenter = new Intl.Segmenter('en', { granularity: 'word' })
  const segments  = [...segmenter.segment(str)]
  let   result    = ''

  for (const { segment } of segments) {
    if ((result + segment).length > maxLength - suffix.length) break
    result += segment
  }

  return result.trimEnd() + suffix
}

But actually: For UI display, CSS is the right answer for truncation:

/* Single line truncation */
.truncate {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Multi-line truncation (all modern browsers) */
.truncate-3 {
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}

11. Shuffling Arrays → Fisher-Yates in 4 Lines

Still installing: lodash (for _.shuffle), array-shuffle

// ✗ What people install
import { shuffle } from 'lodash-es'
shuffle([1, 2, 3, 4, 5])

// ✓ Fisher-Yates shuffle — the correct algorithm, 4 lines
function shuffle(array) {
  const arr = [...array]  // don't mutate the original
  for (let i = arr.length - 1; i > 0; i--) {
    const j     = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]]
  }
  return arr
}

This is the mathematically correct shuffle algorithm. It produces a uniformly random permutation. The lodash implementation is also Fisher-Yates. You’re paying bundle cost for four lines.


12. Checking if a Value Is Empty → A One-Liner

Still installing: lodash (for _.isEmpty)

// ✗ What people install
import { isEmpty } from 'lodash-es'
isEmpty({})     // → true
isEmpty([])     // → true
isEmpty('')     // → true
isEmpty(null)   // → true

// ✓ Native — all the same checks, one expression
const isEmpty = (value) =>
  value == null ||
  (typeof value === 'string'    && value.length === 0) ||
  (Array.isArray(value)         && value.length === 0) ||
  (value instanceof Map         && value.size === 0)   ||
  (value instanceof Set         && value.size === 0)   ||
  (typeof value === 'object'    && Object.keys(value).length === 0)

When NOT to Ditch the Package

This list is not “never install npm packages.” It’s “know what the platform can do before you install.”

Some packages genuinely earn their place:

Keep the package when:
✓ date-fns    — if you need date arithmetic (addDays, startOfWeek, differenceInDays)
✓ lodash      — if you need _.cloneDeep with prototype preservation, _.merge, _.get/_.set
✓ uuid        — if you need UUID v1 (time-based) or v3/v5 (name-based)
✓ zod/yup     — form and API validation has no native equivalent worth using
✓ marked/remark — Markdown parsing is genuinely complex; regex approximations break
✓ axios       — if you need upload progress, complex interceptors, or legacy browser support
✓ chart.js / d3 — visualisation has no native equivalent
✓ any UI library — building a design system from scratch is a real cost

Ask before installing:
? Is there a native API for this?
? Can I write this in under 20 lines?
? What's the actual bundle cost (run npm size or bundlephobia.com)?
? Am I using more than 10% of this library's surface area?

The Quick Reference

What you wantWhat to installWhat to use instead
UUID v4uuidcrypto.randomUUID()
Deep clonelodash.cloneDeepstructuredClone()
Date formatdate-fnsIntl.DateTimeFormat
Relative timedate-fnsIntl.RelativeTimeFormat
Debouncelodash8-line function
Flatten arraylodash.flatten.flat(Infinity)
Group arraylodash.groupByObject.groupBy()
Unique valueslodash.uniqnew Set()
Conditional classesclassnames.filter(Boolean).join(' ')
Number rangelodash.rangeArray.from({ length })
Shuffle arraylodash.shuffleFisher-Yates (4 lines)
Truncate stringlodash.truncateCSS text-overflow
isEmpty checklodash.isEmptyOne-line function

Final Thoughts

The JavaScript platform in 2026 is substantially more capable than it was when most of these packages were first written. structuredClone didn’t exist until 2022. Object.groupBy became Baseline in 2024. crypto.randomUUID() has been in Node.js since 2021.

The packages weren’t installed out of laziness — they were installed because the platform didn’t have good answers yet. Now it often does, and the habit of reaching for npm before checking the platform is worth reconsidering.

Run your bundle through rollup-plugin-visualizer or check package sizes on bundlephobia.com. Find the packages where you’re using one or two functions. Ask if those functions have native equivalents. Delete the package.

The best dependency is no dependency. Not because dependencies are bad, but because a one-liner that ships in the browser doesn’t break, doesn’t have security vulnerabilities, doesn’t add to your bundle size, and doesn’t need to be updated.

Leave a Reply

Your email address will not be published. Required fields are marked *