Essential JavaScript Libraries

These seven packages sit in the dependency trees of most front-end projects. Several were created to fill gaps that the language has since closed, so before npm 2,036 install, ask whether native JavaScript already does the job.

Popular libraries and their native alternatives (npm versions, GitHub 29 stars, npm downloads, September 2026)
Library Licence, stars, downloads/week Native alternative
Lodash 4.18 61,278 : utilities MIT, 61k, 120M structuredClone, Object.groupBy
Ramda 0.32 24,047 : curried functions MIT, 24k, 9.6M Your own pipe (Functional Patterns)
RxJS 7.8 31,705 : observable streams Apache-2.0, 32k, 74M Async iterators (partial)
Immer 11.1 28,983 : immutable updates MIT, 29k, 45M Spread, with(), toSorted()
Zod 4.6 44,027 : schema validation MIT, 44k, 209M None
Axios 1.20 109,234 : HTTP client MIT, 109k, 86M fetch, AbortSignal.timeout()
uuid 14.0: UUIDs v1-v7 MIT, 15k, 207M crypto.randomUUID() (v4)

The repositories are lodash/lodash (https://github.com/lodash/lodash 61,278 ), ramda/ramda (https://github.com/ramda/ramda 24,047 ), ReactiveX/rxjs (https://github.com/ReactiveX/rxjs 31,705 ), immerjs/immer (https://github.com/immerjs/immer 28,983 ), colinhacks/zod (https://github.com/colinhacks/zod 44,027 ), axios/axios (https://github.com/axios/axios 109,234 ) and uuidjs/uuid (https://github.com/uuidjs/uuid 15,334 ). Run the examples as ES modules after npm install lodash-es ramda rxjs immer zod axios uuid.

Lodash and Ramda

Lodash offers hundreds of helpers such as cloneDeep, groupBy, debounce, get and isEqual; import from lodash-es so your bundler can tree-shake the rest. Ramda's functions are curried and take their data last, so they compose into pipelines. Deep copies are now structuredClone() (Copying and Comparing), grouping is Object.groupBy() (Copying Array Methods), and lazy filter-reduce chains are iterator helpers (Iterator Helpers):

Lodash and Ramda next to their native replacementsJavaScript
import { cloneDeep, groupBy } from 'lodash-es';
import * as R from 'ramda';
const sales = [{ city: 'Paris', total: 40, at: new Date() }, { city: 'Oslo', total: 25 },
  { city: 'Paris', total: 90 }];
console.log(cloneDeep(sales)[0].at instanceof Date, structuredClone(sales)[0].at instanceof Date);
console.log(groupBy(sales, 'city').Paris.length, Object.groupBy(sales, (o) => o.city).Paris.length);
const parisTotal = R.pipe(R.filter(R.propEq('Paris', 'city')), R.map(R.prop('total')), R.sum);
console.log(parisTotal(sales), sales.values().filter((o) => o.city === 'Paris')
  .reduce((sum, o) => sum + o.total, 0));
Output
true true
2 2
130 130

Lodash still earns its place for debounce, throttle, isEqual and path access such as get(obj, 'a.b[0]').

RxJS

An Observable pushes many values over time, and RxJS supplies operators that transform, merge and cancel such streams; Angular 42,088 's HttpClient returns them. The classic use is a typeahead that waits for a pause in typing and drops responses to outdated queries:

A debounced typeahead with RxJS operatorsJavaScript
import { Subject, debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
const input$ = new Subject();             // in a browser: fromEvent(input, 'input')
const search = (q) => new Promise((ok) => setTimeout(() => ok(`results for "${q}"`), 30));
input$.pipe(debounceTime(100), distinctUntilChanged(), switchMap(search)) // cancels stale calls
  .subscribe(console.log);
for (const [q, ms] of [['rx', 0], ['rxjs', 60], ['ram', 300]]) setTimeout(() => input$.next(q), ms);
Output
results for "rxjs"
results for "ram"

For one value use a promise; for a sequence you pull, an async iterator (Asynchronous Iteration).

Immer and Zod

Immer hands your recipe a draft: a Proxy (Proxy and Reflect) that records every write. Afterwards it copies only the objects on the changed path, reuses untouched branches (structural sharing, which keeps React 7,897 's === checks cheap) and freezes the result. Zod validates data that TypeScript never sees at compile time, such as form input and API responses; TypeScript in Practice shows how it infers static types from the same schema.

An Immer update with structural sharing, and a Zod schemaJavaScript
import { produce } from 'immer';
import * as z from 'zod';
const state = { user: { name: 'Ada' }, cart: [{ sku: 'A1', qty: 1 }, { sku: 'B7', qty: 3 }] };
const next = produce(state, (draft) => { draft.cart[0].qty += 1; });   // "mutate" a draft
console.log(state.cart[0].qty, next.cart[0].qty, next.user === state.user, Object.isFrozen(next));
const Signup = z.object({ email: z.email(), age: z.number().int().min(13),
  plan: z.enum(['free', 'pro']).default('free') });
console.log(Signup.parse({ email: 'ada@example.com', age: 36 }));
console.log(Signup.safeParse({ email: 'nope', age: 12.5 }).error.issues.map((i) => i.path[0]));
Output
1 2 true true
{ email: 'ada@example.com', age: 36, plan: 'free' }
[ 'email', 'age' ]
Axios and uuid

Axios adds JSON parsing, timeouts, interceptors and rejection on 4xx/5xx responses; internally it picks an adapter (XMLHttpRequest in browsers, http in Node.js 2,131 , or fetch). Native fetch (AJAX with Fetch) with AbortSignal.timeout() (Practical Asynchrony) covers most cases if you check response.ok. crypto.randomUUID() (Web Crypto, secure contexts) replaces uuid for random v4 IDs; keep the package for v7 UUIDs, whose leading millisecond timestamp makes them sort in creation order.

Axios versus fetch with a timeout; crypto.randomUUID() versus uuid v7JavaScript
import http from 'node:http';
import axios from 'axios';
import { v7 } from 'uuid';
const server = http.createServer((req, res) =>
  setTimeout(() => res.end('{"ok":true}'), req.url === '/slow' ? 500 : 0)).listen(8765);
const url = 'http://localhost:8765';
console.log((await axios.get(url, { timeout: 200 })).data);         // parsed JSON
await axios.get(`${url}/slow`, { timeout: 200 }).catch((e) => console.log(e.code));
const res = await fetch(url, { signal: AbortSignal.timeout(200) });
if (res.ok) console.log(await res.json());                            // check ok yourself
await fetch(`${url}/slow`, { signal: AbortSignal.timeout(200) }).catch((e) => console.log(e.name));
server.close();
console.log(crypto.randomUUID(), v7());         // random v4, time-ordered v7
Output
{ ok: true }
ECONNABORTED
{ ok: true }
TimeoutError
1d2676f6-32e3-400e-9ede-319f70162a32 01a0a5ab-30bd-7546-9526-6774b07d924c