Why v4 Is Not a Cosmetic Upgrade
Filament v4 ships a unified Schema API that replaces the parallel Form / Infolist component trees from v3. If you have large resources with custom fields, repeated make() factories, or inline closures scattered across form() and infolist() methods, you will touch almost every resource file. The good news: the migration is mechanical once you understand the three core shifts.
Breaking Change 1 — Schema Replaces Parallel Component Trees
In v3 you maintained two separate component hierarchies:
// v3
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('name')->required(),
]);
}
public static function infolist(Infolist $infolist): Infolist
{
return $infolist->schema([
TextEntry::make('name'),
]);
}
In v4 both methods receive a Schema object and share the same component namespace when you opt into the unified path:
// v4
use Filament\Schema\Schema;
public static function form(Schema $schema): Schema
{
return $schema->components([
TextInput::make('name')->required(),
]);
}
public static function infolist(Schema $schema): Schema
{
return $schema->components([
TextEntry::make('name'),
]);
}
The ->schema() method still works as an alias during the transition period, but relying on it will generate deprecation notices and will be removed in a future minor.
Breaking Change 2 — Action mountUsing and fillForm Signatures
v3 actions used mountUsing to pre-fill a modal form:
// v3
Action::make('approve')
->mountUsing(fn (ComponentContainer $form, Model $record) =>
$form->fill(['note' => $record->last_note])
)
v4 replaces ComponentContainer with Schema and renames the hook:
// v4
use Filament\Schema\Schema;
Action::make('approve')
->fillForm(fn (Model $record): array => [
'note' => $record->last_note,
])
->form([
Textarea::make('note')->required(),
])
mountUsing is removed entirely. Any resource or page that injects ComponentContainer will throw a class-not-found error at runtime — grep your codebase before upgrading.
Breaking Change 3 — Removed ->columns() Shorthand on Groups
v3 allowed Grid::make()->columns(2) as a fluent shorthand. v4 enforces Grid::make(2) as the canonical constructor argument:
// v3 — still parses but deprecated
Grid::make()->columns(2)
// v4 — correct
Grid::make(2)->schema([...])
This is a silent runtime regression in v3 compatibility mode: the grid renders as a single column without an error. Add a search for ->columns( on Grid instances to your pre-upgrade checklist.
Refactor Pattern: Extract a Shared Schema Method
When form and infolist share 80 % of their fields, extract a private static method rather than duplicating:
private static function coreFields(bool $readonly = false): array
{
return [
TextInput::make('name')
->required()
->disabled($readonly),
DatePicker::make('published_at')
->disabled($readonly),
];
}
public static function form(Schema $schema): Schema
{
return $schema->components(static::coreFields());
}
public static function infolist(Schema $schema): Schema
{
return $schema->components(static::coreFields(readonly: true));
}
This pattern eliminates drift between the two views and makes future field additions a single-line change.
Updating Pest Tests
Filament's test helpers mirror the API changes. The assertFormFieldExists and assertInfolists assertions now accept a Schema context:
it('renders the name field in the form', function () {
livewire(EditPost::class, ['record' => Post::factory()->create()])
->assertFormFieldExists('name')
->assertFormFieldIsRequired('name');
});
No change needed here — the helpers are backward-compatible. What does break is any test that directly instantiates ComponentContainer:
// Remove this pattern entirely
$container = ComponentContainer::make($livewire)->statePath('data');
Replace with the Livewire component test helpers; they handle schema resolution internally.
Takeaways
- Grep for
ComponentContainer,mountUsing, and->columns(onGridbefore touching anything else. - The
Schematype hint is the single biggest mechanical change — a project-wide find-and-replace handles 90 % of it. - Extract shared field arrays into private static methods to avoid form/infolist drift.
fillForm(fn)replacesmountUsingfor action pre-population; the old hook is gone, not deprecated.- Pest test helpers are largely compatible; only direct
ComponentContainerinstantiation breaks.