Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Async Column Laravel Package

giacomomasseron/filament-async-column

Filament Async Column loads table column values asynchronously, so slow or expensive data (API calls, heavy queries, computed stats) no longer blocks your Filament tables from rendering. Cells show a placeholder and fill in when ready, keeping admin lists fast and responsive.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Confirm the gate before anything else. The package targets Filament v5. Check your composer.json:

composer show filament/filament | grep versions

If you're on v4 or below, stop — an unsupported major is a blocker, not a risk to manage.

2. Install.

composer require giacomomasseron/filament-async-column

There is no asset build step, no JS bundle, and no migration. The loaders are pure CSS, so npm run build is not part of the install.

3. Pick one genuinely slow column. Do not start with a column that resolves in 5ms — a loader that flashes is worse than no loader. Good first candidates:

  • A shipment/tracking ETA from a carrier API.
  • A credit score or enrichment value from a third party.
  • A computed attribute that fans out across a heavy relationship.

4. Convert a single column. Minimal working example:

use GiacomoMasseroni\AsyncColumn\Column\AsyncColumn;
use Filament\Tables\Columns\TextColumn;

// before
TextColumn::make('shipment_eta'),

// after
AsyncColumn::make('shipment_eta')
    ->resolveUsing(fn ($record) => app(Carrier::class)->eta($record->tracking_number)),

Reload the table. The table should paint immediately and each cell should show the default skeleton loader, then the resolved value.

Where to look first:

  • The AsyncColumn class itself — it's a small presentation-layer wrapper, and reading it end-to-end answers most questions the docs don't.
  • The Loader enum (GiacomoMasseroni\AsyncColumn\Enums\Loader) — skeleton, spinner, dots, bar.
  • vendor/giacomomasseron/filament-async-column/.gitattributes / composer.json — confirms what ships at runtime, which matters if you plan to audit it.
  • The v1.1.2 release notes for the loader surface.

First use case to ship: one column, one table, loader() left at the default. Resist styling on day one; get the perf number first.


Implementation Patterns

Wrap the resolver in cache from the start

The package governs presentation while a value resolves. It does not cache, batch, queue, or throttle. That is entirely yours, and it belongs inside the closure:

AsyncColumn::make('credit_score')
    ->resolveUsing(fn ($record) => Cache::remember(
        "credit-score:{$record->id}",
        now()->addMinutes(15),
        fn () => app(ScoreService::class)->for($record->email),
    )),

Treat resolveUsing() as a cache read, not an API call. If you write the naive version first, you will ship an N+1 against a rate-limited third party.

Handle failure inside the closure

The documented behavior covers the happy path only. Don't rely on the package to render a bounded error state — make the closure total:

->resolveUsing(function ($record) {
    try {
        return app(Carrier::class)->eta($record->tracking_number) ?? '—';
    } catch (Throwable $e) {
        report($e);
        return '—';
    }
})

Add an explicit timeout to the HTTP client behind the service so a hung call can't leave a cell spinning forever.

Per-row loader conventions

loader() and loaderColor() accept closures evaluated per row, which lets you turn loader styling into a documented convention rather than a per-table decision:

use GiacomoMasseroni\AsyncColumn\Enums\Loader;

AsyncColumn::make('shipment_eta')
    ->resolveUsing(fn ($record) => $this->carrier->eta($record))
    ->loader(Loader::Spinner)          // or ->loader('spinner')
    ->loaderColor('primary');          // or a closure: fn ($record) => ...

A workable convention: skeleton (the default) for background enrichment, spinner for values a user is actively waiting on, and a severity tint for values that have a meaningful "late" state (amber for a stale ETA). Write it down in your team docs; otherwise every table drifts.

Keep the styling closures pure

loader() and loaderColor() closures run per row and are styling decisions. Do not put I/O, cache() calls, or policy checks in them — put that logic in resolveUsing() and have the loader closure read from the already-resolved record.

Extract repeated column definitions

If the same async column shape appears across resources, wrap it:

// app/Filament/Columns/CarrierEtaColumn.php
public static function make(string $name = 'shipment_eta'): AsyncColumn
{
    return AsyncColumn::make($name)
        ->resolveUsing(fn ($record) => /* cached lookup */)
        ->loader(Loader::Spinner)
        ->loaderColor('primary');
}

This keeps the cache key, error handling, and loader convention in one place, and makes it trivial to swap back to TextColumn::make() if you ever drop the dependency.

Formatting the resolved value

Whatever you return from resolveUsing() is what the cell renders, so do the formatting there and return a string:

->resolveUsing(fn ($record) => $eta
    ? $eta->timezone($record->timezone)->format('D, M j · H:i')
    : '—')

If you want badges, icons, or rich HTML in a cell, verify against your Filament version whether the column's usual formatters apply to the async-rendered value before designing around them — the safe fallback is a plain formatted string.

Roll out in this order

  1. Bump the dependency in isolation — it's explicitly backward compatible, so it should be a no-op diff.
  2. Measure on one slow table: instrument or log inside resolveUsing() and count invocations per table interaction (initial load, sort, filter, search, paginate, poll). This is the number that decides whether you need a longer cache TTL.
  3. Pilot loader() / loaderColor() on that one table.
  4. Broaden only after the cache layer is in front of every call.

Gotchas and Tips

loadingState() silently beats loader(). Precedence is explicit in the release notes, and it's the right call for backward compatibility — but it means a table with existing custom loading markup will ignore your new spinner. Inventory every loadingState() usage before design specifies a loader that never renders.

The load problem is hidden, not solved. The loader presets change perceived performance, not call volume. A 25-row page against a rate-limited API is still 25 calls unless you cache. Instrument before you broaden adoption.

Re-render amplification is the big unknown. Whether resolveUsing() fires once per page load or on every Livewire re-render (sort, filter, search, pagination, poll) determines your real call multiplier — and it isn't documented. Measure it on a real table with a counter; assume it fires more than once until proven otherwise, and cache accordingly.

loaderColor() is an ordering dependency. Colors resolve through Filament's palette, including entries added via FilamentColor::register(). Panel color registration must happen before the table renders. Also verify light and dark mode for custom colors — low-contrast tints on a skeleton are the likely cosmetic defect, and it's easy to miss because the skeleton is faint by design.

Fast values should not get a loader. If the resolved value typically arrives in under ~100ms, a loader flashes and makes the table feel slower. Either leave those columns as a plain TextColumn with a synchronous value, or cache them warm so the async path returns instantly.

Async values aren't in the query. A column resolved client-side can't participate in database sorting, searching, or filtering the way a real column can. If a user must sort or filter by that value, it needs to be a persisted, indexed column — or you accept that the column is display-only. Check ->sortable() / ->searchable() behavior explicitly; don't assume it degrades gracefully.

Check exports. Filament's CSV/Excel export paths may not traverse an async resolver the same way the table cell does. If that column matters in exports, test it or exclude it deliberately.

Unregistered colors — verify the failure mode. It's unclear whether an unknown color fails loudly or falls back silently. Use a registered palette color (primary, warning, etc.) until you've confirmed, and don't ship a custom hex from a design spec without a smoke test.

Accessibility comes free — don't break it. All four presets stop animating under prefers-reduced-motion, and colors follow dark mode. If you customize via loadingState() for bespoke visuals, you own re-implementing both behaviors.

Escape hatch for bespoke visuals. The presets are deliberately pure CSS. If design wants branded illustrations or non-CSS assets, loadingState() is the supported extension point and is unchanged by v1.1.2 — but plan for the reduced-motion and dark-mode work yourself.

Dependency risk is real but bounded. Single maintainer, ~10 stars, zero dependents, MIT. The code surface is small, which caps the worst case. Pin to a tested minor, keep the one-afternoon fallback in mind (revert those columns to TextColumn::make() with a preloaded or synchronous value), and don't let the async column become load-bearing for a feature that can't survive a revert.

Debugging checklist when a cell stays on the skeleton:

  1. Confirm resolveUsing() is actually firing — Log::info() at the top of the closure.
  2. Confirm the closure returns a non-null string; a null return may look indistinguishable from "still loading."
  3. Check whether an ancestor column has loadingState() set.
  4. Watch the network tab for the Livewire request and see whether it completes.
  5. If it hangs, the resolver is probably blocking on a call with no timeout — fix that in the service, not the column.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource