Small JavaScript helpers can remove repetitive code without adding another dependency. They are most useful when the behavior is clear, used in more than one place, and easy to test. For parsing dates, handling URLs, or formatting numbers, prefer the built-in APIs instead of writing a custom parser.
The examples below cover common array transformations, delayed input handling, query parameters, and localized output. Each helper is intentionally small; adapt its input checks and behavior to your application rather than treating it as a general-purpose library.
Choose a helper for the task
| Task | Approach | Important detail |
|---|---|---|
| Split an array into batches | chunk | The batch size must be a positive integer |
| Keep the first item for each key | uniqueBy | Keys use Set equality, not string conversion |
| Collect items by a key | groupBy | A Map safely supports non-string keys |
| Wait until input pauses | debounce | It delays work; it does not cancel work already started |
| Change a URL parameter | URLSearchParams | set replaces existing values for that parameter |
| Format money or dates | Intl | Choose a locale, currency, and time zone deliberately |
Split an array into batches
Batching is useful when you need to render a long list in pages, send records in bounded requests, or process items in smaller groups. This version returns a new array and rejects invalid batch sizes instead of getting stuck in a loop.
function chunk(items, size) {
if (!Array.isArray(items)) {
throw new TypeError("items must be an array");
}
if (!Number.isInteger(size) || size < 1) {
throw new RangeError("size must be a positive integer");
}
const batches = [];
for (let index = 0; index < items.length; index += size) {
batches.push(items.slice(index, index + size));
}
return batches;
}
console.log(chunk(["a", "b", "c", "d", "e"], 2));
// [["a", "b"], ["c", "d"], ["e"]]
An empty array returns an empty array. The final batch may be shorter than the requested size. The helper uses Array.prototype.slice(), which copies each selected portion; it does not mutate the input array.
Keep the first item for each key
When an API response contains repeated records, use a key selector to define what counts as a duplicate. This example keeps the first record for each id and preserves the order of those first appearances.
function uniqueBy(items, getKey) {
const seen = new Set();
return items.filter((item) => {
const key = getKey(item);
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
const users = [
{ id: 7, name: "Mina" },
{ id: 8, name: "Ravi" },
{ id: 7, name: "Mina (updated)" },
];
console.log(uniqueBy(users, (user) => user.id));
// [{ id: 7, name: "Mina" }, { id: 8, name: "Ravi" }]
Set compares keys using SameValueZero semantics. That works well for strings, numbers, and stable IDs. If the selector returns objects, two separately created objects are different keys even if their fields match; select a primitive identifier when you want records grouped by value.
Group records without turning keys into object properties
Grouping is similar to deduplication, but retains every item for a key. Returning a Map avoids coercing every key to a string and handles values such as numbers or object references.
function groupBy(items, getKey) {
const groups = new Map();
for (const item of items) {
const key = getKey(item);
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key).push(item);
}
return groups;
}
const tasks = [
{ title: "Fix form", done: false },
{ title: "Write tests", done: true },
{ title: "Update copy", done: false },
];
const tasksByState = groupBy(tasks, (task) => task.done);
console.log(tasksByState.get(false));
// [{ title: "Fix form", done: false }, { title: "Update copy", done: false }]
Map keeps insertion order and supports keys of any type. Use an object-shaped result only when your keys are known strings and that output shape is more convenient for the next step.
Debounce repeated input
Debouncing waits until calls stop arriving for a chosen delay before running the callback. It is useful for reducing redundant work while someone types into a search field. It is not a replacement for server-side rate limits, and it does not stop a callback that has already begun.
function debounce(callback, delay = 250) {
let timer;
function debounced(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
timer = undefined;
callback(...args);
}, delay);
}
debounced.cancel = () => {
clearTimeout(timer);
timer = undefined;
};
return debounced;
}
const logSearch = debounce((term) => console.log(`Search for: ${term}`));
logSearch("jav");
logSearch("javascript");
// After the delay, only "Search for: javascript" is logged.
In a browser, pass the current input value to the debounced function from an input event handler. Call logSearch.cancel() when the view is removed if a pending callback should not run. This implementation does not preserve a method's this value or return the callback's result; use a more specific implementation if your callback depends on either.
The delay uses setTimeout(). A timer is a scheduling request, not an exact clock: a busy page or runtime can run it later than the requested delay.
Update query parameters with the URL API
Avoid building query strings with manual concatenation. URLSearchParams handles encoding and existing parameters for you. This helper changes one value in a URL object and treats null as a request to remove it.
function setQueryParam(url, name, value) {
if (value === null) {
url.searchParams.delete(name);
} else {
url.searchParams.set(name, String(value));
}
return url;
}
const url = new URL("https://example.test/search?q=tools");
setQueryParam(url, "page", 2);
setQueryParam(url, "q", "JavaScript utilities");
console.log(url.toString());
// https://example.test/search?q=JavaScript+utilities&page=2
URL accepts a relative reference when you provide a base URL to its constructor. The resulting URL exposes query parameters through URLSearchParams. searchParams.set() replaces existing values for that name; use append() when repeated parameters are intentional. If you write the result to browser history with History.pushState() or History.replaceState(), the URL must be same-origin.
Format values for the reader's locale
The built-in Intl formatters handle separators, currency placement, and date presentation for a locale. Prefer them over hard-coded commas, symbols, and month names.
const money = new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
});
const date = new Intl.DateTimeFormat("en-GB", {
dateStyle: "medium",
timeZone: "UTC",
});
console.log(money.format(1234.5));
// £1,234.50
console.log(date.format(new Date("2026-09-24T00:00:00Z")));
// 24 Sept 2026
Intl.NumberFormat uses the currency code to choose the currency and the locale to choose its presentation. Intl.DateTimeFormat uses the runtime's local time zone unless you set one. Specify a time zone when a date represents a fixed instant or a business rule rather than the reader's local time.
Check runtime compatibility
These examples use modern JavaScript features and built-in web APIs. Before shipping them, check the compatibility data for the browsers, embedded webviews, or Node.js versions your project supports. Transpiling syntax does not automatically provide missing runtime APIs such as Map, URL, or Intl features; use a polyfill only when your support policy requires it.
When a utility library is a better choice
Use a small helper when its rules fit your application and you can explain its edge cases. Prefer an established, maintained library when you need a broad set of tested operations, complex date or time-zone rules, or behavior shared across several projects. In either case, check the package's maintenance status, license, bundle impact, and supported runtimes before adding it.
For a focused helper, write down what happens for empty input, invalid arguments, duplicate keys, and cancellation. A short test for those boundaries is more useful than copying a large collection of helpers that the project does not need.