Skip to content

Await invalidations after mutations - #3354

Draft
david-crespo wants to merge 1 commit into
mainfrom
prototype-mutation-invalidations
Draft

Await invalidations after mutations#3354
david-crespo wants to merge 1 commit into
mainfrom
prototype-mutation-invalidations

Conversation

@david-crespo

@david-crespo david-crespo commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Would close #3083. Experimenting with this while investigating a CI e2e flake on the silo IP pools tab. Leaving as a draft because I need to make sure I like it and I don't know about these tests.

Basically there are things in the UI that should wait for the invalidation after a mutation to be done, but currently we don't await invalidateEndpoint calls anywhere. It's hard for users to have a real problem with this because they don't click fast enough to beat the async invalidation, but it affects tests, and it also means that there is some visual pop-in after a side-modal closes but before the invalidation comes back.

🤖 description of invalidation race in e2e test

After unlinking ip-pool-1, the confirm modal closes before the invalidated queries finish refetching.

The test then opens ip-pool-2’s action menu. One of those late refetches updates the data used to build the row actions, replacing the action column and unmounting the open menu. Playwright either:

  • finds “Clear default,” but it detaches while being clicked, or
  • completes the click, but the confirmation dialog never opens.

The unlink itself succeeds; the failure is the UI becoming unstable during the unawaited refetch. Awaiting invalidation keeps the mutation—and therefore the modal—pending until those refetches settle.

We could manually await invalidations in all onSuccess callbacks (and make all success callbacks async) but the approach in this PR is neater and harder to mess up. When we do it manually, there's nothing forcing us to put the invalidations first. By defining them in this structured way, the useApiMutation helper ensures the invalidations always run first and it runs them in parallel instead of sequentially.

New invalidation API

useApiMutation(api.foo, {
  invalidateEndpoints: ['fooList', 'fooView'],
  onSuccess() {
    // runs after refetches finish
  },
})

UX considerations

Even when there isn't a bug actively caused by not awaiting, it still makes the UI jump around unnecessarily. One thing worth considering is whether we should find a way to do a kind of temporary glowy flash on new rows when they appear in the table. That would be kind of nice, especially now that the modal closing and the pop-in are simultaneous.

Before

It's subtle, but you can see that after creating a floating IP, the side modal closes and then the new floating IP pops into the list.

2026-08-27-await-invalidation-before.mp4

After

After successful creation, the side modal closes and the new floating IP appears at the same time.

2026-08-27-await-invalidation-after.mp4

@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
console Ready Ready Preview Aug 27, 2026 9:57pm

Request Review

@david-crespo
david-crespo force-pushed the prototype-mutation-invalidations branch from 308dbf8 to c75ba35 Compare August 27, 2026 00:03
david-crespo added a commit that referenced this pull request Aug 27, 2026
Spun off from #3354, where I wanted `Promise.withResolvers` in a test
but tsc wouldn't let me. Then I found out our browserslist list setting
is not being used by anything — autoprefixer went away with Tailwind v4.

### Changes

- `target` in tsconfig: es2023 → es2024, so tsc accepts ES2024 builtins
like `Promise.withResolvers`
- Set `build.target: 'es2024'` in the Vite config to match. Vite's
default target is
[`'baseline-widely-available'`](https://vite.dev/config/build-options.html#build-target),
a browser list regenerated for each Vite major (currently Chrome 111 /
Firefox 114 / Safari 16.4)
- Add a safety test asserting the two targets stay equal
- Remove the browserslist key and the autoprefixer dep 

### Argument

`target` in tsconfig can control two things: what syntax tsc emits and
the default `lib` declarations for typechecking. We use `noEmit` because
we compile with Vite, so we only care about `lib`: it determines which
builtin APIs the typechecker allows (`Promise.withResolvers`,
`Object.groupBy`, etc.).

Vite has its own target concept, and it [only affects
syntax](https://oxc.rs/docs/guide/usage/transformer/lowering#warnings):
it rewrites newer syntax for older browsers, but if you call a too-new
API, it doesn't polyfill or warn about it. 🤖 confirmed this by building
`Promise.withResolvers()` with target `safari16.4`: clean build, the
call ships as-is, TypeError at runtime. That means tsc's `lib` is the
only thing keeping unsupported APIs out of the code, so the tsconfig
target has to line up with the browsers we actually support. This PR
sets `build.target: 'es2024'`, which Vite maps to the oldest browsers
with full ES2024 support (Chrome 119, Safari 17.4, Firefox 145, per
https://caniuse.com/sr-es15), and adds a safety test asserting the two
targets are the same.

The potential downside is that we are moving our target to browsers that
might be too new. The worst one is Firefox, where full ES2024 support
means 145 (Nov 2025). But that version requirement comes from a single
feature that we're never going to call: [per
MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync#browser_compatibility),
`Atomics.waitAsync` appeared in Firefox 145, while every other ES2024
feature was in by Firefox 128 (July 2024), and the ones we'd actually
use (`groupBy`, `withResolvers`, `isWellFormed`) by 121 (Dec 2023).

### Alternatives

* Do nothing (but still remove useless browserslist), i.e., leave
`es2023` for TS and `baseline-widely-available` for Vite. Fine, but
`es2024` has fun APIs we could use.
* `es2024` for TS but leave Vite on the default. Also most likely fine
in practice, but feels bad.
@david-crespo david-crespo changed the title Prototype awaited mutation invalidations Await invalidations after mutations Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider invalidating all queries on refresh instead of listing relevant ones

1 participant