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.
| 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):
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));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:
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);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.
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]));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.
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{ ok: true }
ECONNABORTED
{ ok: true }
TimeoutError
1d2676f6-32e3-400e-9ede-319f70162a32 01a0a5ab-30bd-7546-9526-6774b07d924c