components/underscore
A PHP port of Underscore.js offering a rich set of chainable utility functions for arrays, collections, strings, and objects. It brings familiar JavaScript helpers like map, filter, reduce, and pluck to Laravel and PHP projects, simplifying common data manipulation tasks.
Underscore is a single-file JavaScript utility library — it has nothing to do with Composer. In a Laravel codebase you consume it through your front-end build, not through composer require. The components/underscore entry is the deprecated Component/Bower-era distribution; install the canonical npm package instead and treat the GitHub repo (documentcloud/underscore) as the source of truth.
Minimal setup:
npm install underscore
Then wire it into your Vite (Laravel 9+) or Mix (older) pipeline in resources/js/app.js:
import _ from 'underscore';
window._ = _; // only if you have legacy Blade inline scripts that expect a global
Build with npm run dev / npm run build, and reference the bundle in your layout with @vite('resources/js/app.js') (or {{ mix('js/app.js') }}).
Where to look first:
underscore.js at the repo root — it is one annotated UMD file, roughly 1,700 lines. You can genuinely read the whole thing in an afternoon.underscorejs.org — every function is documented inline with the implementation beside it.node_modules/underscore/package.json to confirm the version (1.13.x is current) before relying on newer methods like _.chunk or _.isNaN semantics.First use case: a live-search box. You have a Blade-rendered input that hits a Laravel route on every keystroke. Without a debounce you DDoS your own app. With Underscore:
import _ from 'underscore';
const search = _.debounce(async (term) => {
const { data } = await axios.get('/api/search', { params: { q: term } });
renderResults(data);
}, 300);
document.querySelector('#search').addEventListener('input', (e) => search(e.target.value));
That single function justifies the dependency on day one. Everything else — _.pluck, _.groupBy, _.template — is convenience on top.
Prefer explicit ESM imports in every file that needs it:
import { debounce, groupBy, sortBy } from 'underscore';
Named imports read better than _ and survive a future migration to lodash-es or hand-rolled helpers with a find-and-replace. Reserve the window._ global for one legacy reason only: Blade partials that contain inline <script> blocks you are not ready to refactor into modules. Be honest with yourself about how many of those you have — if it is more than a handful, that is the real tech-debt ticket, not the library choice.
This is the highest-value pattern in a Laravel app, because every one of these handlers hits a controller:
// Autosave a draft every 2s of idle typing, but guarantee a flush every 15s
const saveDraft = _.debounce(async (id, body) => {
await axios.patch(`/drafts/${id}`, { body });
}, 2000);
const flush = _.throttle(() => saveDraft.flush(), 15000);
Note _.debounce and _.throttle return functions carrying .cancel() and .flush(). Call .cancel() in your component teardown / turbo:before-cache / Livewire destroy hook or you will leak timers across page transitions.
Laravel's API resources tend to return flat arrays of objects. Underscore excels at reshaping them for a chart or a select box without a server round-trip:
import { groupBy, sortBy, map, reduce } from 'underscore';
// Group orders by status, then sort each bucket by total desc
const byStatus = groupBy(orders, 'status');
const buckets = Object.fromEntries(
Object.entries(byStatus).map(([status, rows]) => [
status,
sortBy(rows, (o) => -o.total),
])
);
// Shape a payload before POSTing
const payload = map(rows, (r) => pick(r, 'sku', 'qty', 'unit_price'));
_.pick / _.omit are the workhorse pair here — using them to build request bodies from a big Eloquent-serialized object keeps controllers from needing a second FormRequest just to whitelist fields.
When a transformation is more than two steps, switch to chain syntax so it reads top-down instead of inside-out:
import _ from 'underscore';
const topCustomers = _.chain(orders)
.filter((o) => o.status === 'paid')
.groupBy('customer_id')
.map((rows, id) => ({ id, total: _.reduce(rows, (s, r) => s + r.total, 0) }))
.sortBy((c) => -c.total)
.first(10)
.value();
The .value() at the end is mandatory — forgetting it is the single most common Underscore bug. If you find yourself chaining constantly, that is a signal the logic belongs in a Laravel query or an API resource instead.
_.template is genuinely useful for client-side partials — e.g. rendering a row after an AJAX create without reloading. The trap is delimiter collision: Underscore defaults to <%= %>, which is safe with Blade, but if you reconfigure to {{ }} (common when coming from Handlebars or Vue) Blade will try to compile it server-side first.
_.templateSettings.interpolate = /\{\{(.+?)\}\}/g;
{{-- Blade will attempt to compile the {{ }} above unless you escape it --}}
<script>
_.templateSettings.interpolate = /\@\{\{(.+?)\}\}/g;
</script>
Simpler rule: keep Underscore's default <%= %> / <%- %> delimiters and never fight Blade. Use <%- %> (escape) for anything that originated from a user, and <%= %> only for values you produced yourself.
_.mixin is the clean extension point for app-specific helpers, and it participates in chaining automatically:
import _ from 'underscore';
_.mixin({
money(cents) {
return (cents / 100).toLocaleString('en-US', { style: 'currency', currency: 'USD' });
},
initials(name) {
return name.split(' ').map((p) => p[0]).join('').toUpperCase();
},
});
_.chain(users).pluck('name').map(_.initials).value();
Keep these in resources/js/lib/underscore-mixins.js and import it once in app.js.
If you maintain a Laravel 5/6 app that still ships jQuery, Underscore slots in with zero ceremony — both attach to window, both are synchronous, and Underscore's _.each on a jQuery collection works because it is array-like. Use _.debounce on $(window).on('resize') and _.groupBy on .data() results. No adapter needed.
The package entry you were given is not the one to install. components/underscore is a Component-registry mirror from the Bower era, long unmaintained. npm install underscore gives you the same code from the actual maintainers. Do not add a Component or Bower step to a Laravel project in 2024 — you will be the only one who knows how it works.
It will not tree-shake. Underscore ships as UMD/CommonJS. Vite and Webpack will include all ~17 KB gzipped regardless of whether you import one function or twenty. If bundle size is a real constraint, lodash-es gives you per-method ESM imports, or just use native Array.prototype.map/filter/reduce for the simple 80% and keep Underscore only for debounce/throttle/template.
_.template is an XSS vector if misused. <%= %> inserts raw. Anything that traces back to a user — a comment body, a customer name from your database, a query-string echo — must go through <%- %> or, better, be rendered by Blade on the server where escaping is on by default. Treat client-side templating of user content as a code-review red flag.
Delimiter collisions are silent. If you set _.templateSettings inside a Blade file and used {{ }}, Blade strips it before the browser ever sees it, and your template renders literally as {{ name }}. Symptom: template "works" but prints its own source. Fix by using <%= %> or @{{ }} escaping.
Forgetting .value() on a chain returns the wrapped object, not your result. You will get [object Object] in the DOM or a TypeError two lines later. If a chained expression surprises you, console.log the chain before .value() — Underscore's wrapper prints usefully.
_.debounce loses this if you are careless. The returned function preserves this of the call site, but if you pass a bound method you have already bound, you get double-binding confusion. Also, .cancel() and .flush() are on the returned function, not on the callback — stash the reference:
this.onInput = _.debounce(this.handle, 300);
// later
this.onInput.cancel();
Object iteration order is not guaranteed. _.each over an object, _.keys, _.values — ordering follows the engine's property rules. If order matters (it does for anything you render), iterate arrays or _.sortBy the keys explicitly.
_.isEqual is deep but not cycle-safe. Comparing two Eloquent-serialized objects with circular references (rare, but possible from a Vue reactive proxy) will stack-overflow. _.clone is shallow by default — use _.clone(obj, true) for deep, but remember it does not clone functions or class instances correctly.
Global _ collision. If any part of your stack also loads lodash (very common in older Laravel apps via a select2 or moment plugin), the last one to load wins. Use _.noConflict() or, better, drop the global entirely and rely on module imports.
Debugging tip: Underscore is small enough to read. When behavior surprises you — _.uniq on objects, _.flatten depth, _.range with a negative step — open node_modules/underscore/underscore.js and read the twenty lines. It will take less time than a Stack Overflow search, and the annotated source tells you exactly which algorithm it chose.
Strategic note on the score. A 51 overall with a 19 "opportunity" score is the profile of a mature, finished, low-churn library: 27k stars, MIT, no meaningful feature velocity. That is fine — the API surface is frozen and stable, which is exactly what you want from a utility belt you are not going to upgrade for three years. The risk is not breakage; the risk is that new hires reach for it out of habit and the codebase accumulates two idioms (_ and native) where one would do. Pick a house style and enforce it in review.
How can I help you explore Laravel packages today?