Fix: forget a closed action's cached schema when it is unmounted (#20361)

* uncache mounted schemas

* Refine mounted action schema cache cleanup

Amp-Thread-ID: https://ampcode.com/threads/T-01a04e2a-fe3c-731e-b615-9a4456fdceaa

---------

Co-authored-by: Dan Harrin <git@danharrin.com>
This commit is contained in:
Bartłomiej Gajda
2026-08-29 21:05:49 +02:00
committed by GitHub
parent 1079714778
commit 4d9dd73ddf
3 changed files with 89 additions and 4 deletions
@@ -421,13 +421,22 @@ trait InteractsWithActions /** @phpstan-ignore trait.unused */
$this->mountedActions = [];
$this->cachedMountedActions = null;
foreach ($this->cachedSchemas as $schemaName => $schema) {
if (str($schemaName)->startsWith('mountedActionSchema')) {
$this->forgetCachedMountedActionSchemas();
$this->mountAction($name, $arguments, $context);
}
protected function forgetCachedMountedActionSchemas(int $fromNestingIndex = 0): void
{
foreach (array_keys($this->cachedSchemas) as $schemaName) {
if (! str_starts_with($schemaName, 'mountedActionSchema')) {
continue;
}
if (((int) substr($schemaName, strlen('mountedActionSchema'))) >= $fromNestingIndex) {
unset($this->cachedSchemas[$schemaName]);
}
}
$this->mountAction($name, $arguments, $context);
}
public function cacheAction(Action $action): Action
@@ -803,6 +812,12 @@ trait InteractsWithActions /** @phpstan-ignore trait.unused */
array_pop($this->cachedMountedActions);
}
// The schemas of the actions that have just closed, which are cached by nesting index: an
// action mounted at one of those indexes later in this request would otherwise be handed
// the schema of the action that used to be there, since `getMountedActionSchema()` reads
// the cache before it builds anything.
$this->forgetCachedMountedActionSchemas(fromNestingIndex: count($this->mountedActions));
if (! count($this->mountedActions)) {
$action?->clearRecordAfter();
+42
View File
@@ -483,6 +483,48 @@ describe('nested actions', function (): void {
});
});
describe('unmounting actions', function (): void {
it('forgets a nested action schema before another action is mounted at the same index', function (): void {
$livewire = livewire(Actions::class)
->mountAction('staleSchemaParent')
->mountAction('first');
expect(array_keys($livewire->instance()->getSchema('mountedActionSchema1')->getFlatFields()))
->toBe(['alpha']);
$livewire->call('unmountThenMountAction', 'second');
expect(array_keys($livewire->instance()->getSchema('mountedActionSchema1')->getFlatFields()))
->toBe(['beta']);
});
it('forgets a root action schema before another action is mounted at the same index', function (): void {
$livewire = livewire(Actions::class)
->mountAction('staleSchemaParent');
expect(array_keys($livewire->instance()->getSchema('mountedActionSchema0')->getFlatFields()))
->toBe(['parentField']);
$livewire->call('unmountThenMountAction', 'staleSchemaOther');
expect(array_keys($livewire->instance()->getSchema('mountedActionSchema0')->getFlatFields()))
->toBe(['otherField']);
});
it('preserves the schema of an action that remains mounted', function (): void {
$livewire = livewire(Actions::class)
->mountAction('staleSchemaParent')
->mountAction('first');
$livewire->call('unmountAction');
expect(array_keys($livewire->instance()->getSchema('mountedActionSchema0')->getFlatFields()))
->toBe(['parentField'])
->and($livewire->instance()->getSchema('mountedActionSchema1'))
->toBeNull();
});
});
describe('extra modal footer actions', function (): void {
it('can mount an action that has a group in `extraModalFooterActions()`', function (): void {
livewire(Actions::class)
+28
View File
@@ -121,6 +121,28 @@ class Actions extends Page
])
->action(fn () => null),
]),
Action::make('staleSchemaParent')
->schema([
TextInput::make('parentField'),
])
->registerModalActions([
Action::make('first')
->schema([
TextInput::make('alpha'),
])
->action(fn () => null),
Action::make('second')
->schema([
TextInput::make('beta'),
])
->action(fn () => null),
])
->action(fn () => null),
Action::make('staleSchemaOther')
->schema([
TextInput::make('otherField'),
])
->action(fn () => null),
Action::make('grandparentWithModalCloseCancellation')
->schema([
TextInput::make('grandparentValue')
@@ -281,4 +303,10 @@ class Actions extends Page
}),
];
}
public function unmountThenMountAction(string $name): void
{
$this->unmountAction();
$this->mountAction($name);
}
}