Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

[2.0] Proposal: `independent()` for separate atomic update groups

Aperta
#4,447 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
35/100
Tipo di issue
Funzionalità
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
javascript, typescript
Ambito
frontend

Direzione di ricerca

Read the proposed semantics and the prototype at https://github.com/Monkeylordz/solid/pull/1; the issue does not name repository files or tests. This is a request for feedback on whether the primitive belongs in core and on its semantics and name, so a concrete implementation task would need to be agreed before work can be considered done.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

enhancement

I’d like to propose an independent primitive that lets synchronous writes form an atomic update group that can commit independently of unrelated work in the surrounding batch.

Motivation

The motivating case is a component updating its own state and then invoking a consumer callback:

function selectItem(item) {
  setMenuOpen(false);
  props.onSelect(item); // may trigger async data loading
}

If the callback starts async work, the menu close can be held along with it, even though those updates are unrelated.

As the component author, I want to preserve normal batching while keeping the component’s local update separate from work started by its consumer:

function selectItem(item) {
  independent(() => setMenuOpen(false));
  props.onSelect(item);
}

The menu can then close at the normal microtask flush while the selection waits for data. Reversing those two calls preserves the separation when their reactive graphs do not overlap.

Details

The proposed semantics are:

  • The callback runs synchronously; its writes still use normal batching and effect phases.
  • Writes within a group commit together. Async work triggered and observed through that group can still hold the whole group.
  • Separate or nested calls create separate groups.
  • Independence is conditional: overlapping writes, shared derivations, or writes to already held state can merge groups. Sharing a render effect alone does not merge them.
  • The scope ends when the callback returns. Writes after an await are outside it, and throws do not roll back writes already made.

I have a complete implementation in my development fork for review, you are free to use it in its entirety:
https://github.com/Monkeylordz/solid/pull/1

It almost completely treeshakes away when independent is not imported, and importing it adds approximately 1.04 kB Brotli.

This is also something I think can only be done inside the core signals runtime - a third-party library cannot control which writes share a transaction or how those transactions merge and commit through the existing public APIs.

Alternatives & why they don't work:

I went through my own (private) component library and found roughly 50 places where a component writes state and then calls a props.onX callback. Handling these required different combinations of flush, latest, and setTimeout, depending on the surrounding code.

flush:

Using flush between the component’s write and the consumer callback can separate their updates. However, it drains globally pending work, including ready updates made by the caller:

function select() {
  setA(...);
  flush();
  setB(...);
}

setConsumerState(...);
select(); // also flushes the caller's ready pending work
setOtherConsumerState(...);

The consumer now needs to know that select() contains a flush and account for that when ordering its own writes. Adding or moving an internal flush can change observable behavior outside the component, even though its public API has not changed. However, reordering calls is not always practical: functions take parameters, return values, and perform other side effects that constrain their order. In my library, I ended up making some callbacks return () => void “report” functions. Components had to manage those functions and call them at the end of their handlers so that internal flushes would not interfere with consumer callbacks. Furthermore, this requires coordination across every layer - consumers can add their own flushes and disrupt intended behavior. It is difficult to maintain as a general library pattern.

flush also changes when effects run and can make them run more times than intended.

latest:

latest moves responsibility from the operation that needs independence to the code consuming its state. A component library may expose that state through accessors, controlled props, context, or callbacks. Each relevant reader then needs to know when to use latest, including readers supplied by consumers. Adding a new ordinary read can reintroduce the original behavior.

It also changes which values readers observe without separating the underlying writes into their own commit group. If an operation updates several signals, selectively reading some through latest does not express that those writes should commit together independently of unrelated work.

Finally, latest still follows its own async consistency rules: rendered async work derived from its view can hold that view. It is useful for deliberately displaying in-flight state, but making a component’s update independent through reader annotations requires coordination across the component and its consumers.

setTimeout:

Moving a write into a later task can separate it from the current batch, but introduces several other concerns:

  • Discoverability and scheduling assumptions - Authors would need to know which scheduling boundary produces the intended behavior. For example, queueMicrotask is sensitive to whether Solid’s flush has already been queued, so replacing one scheduling mechanism with another is not straightforward.
  • Visible intermediate states - Deferring to another task permits browser paint and other callbacks to run between the original handler and the deferred update.
  • Cleanup and cancellation - Components must cancel obsolete writes when they are disposed or when a newer interaction supersedes the scheduled one. That adds manual timer bookkeeping or generation checks to otherwise synchronous handlers.
  • Write ordering and stale state - A deferred write runs after writes made inline in the meantime and can overwrite a newer value. Capturing values before scheduling risks stale inputs; reading them later changes which state the operation observes.
  • Lost execution context and intent - A plain timer callback does not automatically retain the originating action’s transaction context. It also gives Solid no explicit indication that the scheduling boundary was intended to separate update groups, making that intent harder to represent in diagnostics.

independent would let the component express that boundary at the write site while preserving synchronous execution and normal batching, reducing the coordination required from callers.


Would this be a useful primitive to support in core? I’d particularly appreciate feedback on the semantics, the name, and interactions that the prototype should cover. Again, a complete implementation is available to see here: https://github.com/Monkeylordz/solid/pull/1.

Lingua principale
TypeScript
Stelle
36.2k
Fork
1.1k
Merge medio
11h 48m
PR unite (30g)
441

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di solidjs/solid

Tutte le issue di solidjs/solid

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.