This is an enhancement issue following this Discord conversation between Ryan and myself.
Usecase
I'm trying to write a router that is as fast for SPAs as the native browser history is for old-fashioned MPAs, especially when a user goes back in their history. For this I want to keep previous pages in memory, including their DOM nodes. However, while they are in an invisible/cached state, I don't want them to trigger fetches etc.
For example, imagine a shopping site. The current "shopping cart" is essentially global state. I visit a page /details that shows me price details of my shopping cart. Part of its implementation behaves somewhat like this:
<For each={shoppingCart()}>
{(item) => <Item price={fetchCurrentPriceForItem(item().id)} name={item().name}/>}
</For>
I then want to add something else to my shopping cart so I navigate to /search?q=something+else. I add it to my shopping cart. Now the shoppingCart() signal updates in the cached /details page, and fetches pricing details I didn't need fetching for. There may also be effects running that are wasting CPU.
My example is very simplified but I had variations of this problem in "real" code (using Solid 1.0) too. I ended up defining a isPageVisible() signal that so any refetch only happens when it is true, but then you have to wire that signal through all your effects. This also only solves the issue for effects/resources, but Solid still "wastes" CPU cycles updating other parts of the DOM.
Proposed solution
A minimal API:
<PropagationBoundary block={isPageVisible()}>
...
</PropagationBoundary>
When block == true, all elements nested within the PropagationBoundary will no longer update, run any effects or do any async stuff. Alternatively we can name it Freeze to signal that its contents will be frozen regardless of state changes.
In the Discord thread we also talked about more exotic extra options, for example only running effects on animationFrame. I'm currently not sure how that would fit within this concept of a propagation boundary.
I'd also be fine with this being an optional argument block: () => boolean when creating an Owner. It may simplify the implementation, and I'm creating an Owner for each page anyway.
Considerations
Updates flowing "upwards"
Coming back to the example, shoppingCart is "top-level" state and its updates travel "down" to the page and eventually its <Item/> components. If at any point an active propagation boundary is encountered, updates shouldn't propagate. Conceptually quite simple.
However, an update may also trigger "within" a propagation boundary and travel "upward". For example: the /details page somehow triggers an update to a signal that is displayed in the header of the page. This should naturally occur less frequently, but we should clearly define what happens anyway: do we block it or not? Personally I think there are arguments for both cases.
Intuitively I'd expect it is easiest to ignore the source of the update/write. The rule would be simple then: if a read happens inside an active propagation boundary, it will never see new state. This intuition is however based on Solid 1.0, so take it with a grain of salt.
Escape hatch
I proposed a withoutBoundary helper to Ryan that would unblock propagation while in an active propagation boundary. I currently don't see a need for it anymore, unless the behaviour for "upwards" updates becomes more complex than what I suggested above.
Alternative name: "Freeze"
Maybe PropagationBoundary is rather technical, and it's better to describe the resulting behaviour rather than the way it is implemented. In that case I'd suggest Freeze, as it "freezes" the contents and any associated effects. Happy to hear your thoughts.
This is an enhancement issue following this Discord conversation between Ryan and myself.
Usecase
I'm trying to write a router that is as fast for SPAs as the native browser history is for old-fashioned MPAs, especially when a user goes back in their history. For this I want to keep previous pages in memory, including their DOM nodes. However, while they are in an invisible/cached state, I don't want them to trigger fetches etc.
For example, imagine a shopping site. The current "shopping cart" is essentially global state. I visit a page
/detailsthat shows me price details of my shopping cart. Part of its implementation behaves somewhat like this:I then want to add something else to my shopping cart so I navigate to
/search?q=something+else. I add it to my shopping cart. Now theshoppingCart()signal updates in the cached/detailspage, and fetches pricing details I didn't need fetching for. There may also be effects running that are wasting CPU.My example is very simplified but I had variations of this problem in "real" code (using Solid 1.0) too. I ended up defining a
isPageVisible()signal that so any refetch only happens when it is true, but then you have to wire that signal through all your effects. This also only solves the issue for effects/resources, but Solid still "wastes" CPU cycles updating other parts of the DOM.Proposed solution
A minimal API:
When
block == true, all elements nested within thePropagationBoundarywill no longer update, run any effects or do any async stuff. Alternatively we can name itFreezeto signal that its contents will be frozen regardless of state changes.In the Discord thread we also talked about more exotic extra options, for example only running effects on animationFrame. I'm currently not sure how that would fit within this concept of a propagation boundary.
I'd also be fine with this being an optional argument
block: () => booleanwhen creating anOwner. It may simplify the implementation, and I'm creating anOwnerfor each page anyway.Considerations
Updates flowing "upwards"
Coming back to the example,
shoppingCartis "top-level" state and its updates travel "down" to the page and eventually its<Item/>components. If at any point an active propagation boundary is encountered, updates shouldn't propagate. Conceptually quite simple.However, an update may also trigger "within" a propagation boundary and travel "upward". For example: the
/detailspage somehow triggers an update to a signal that is displayed in the header of the page. This should naturally occur less frequently, but we should clearly define what happens anyway: do we block it or not? Personally I think there are arguments for both cases.Intuitively I'd expect it is easiest to ignore the source of the update/write. The rule would be simple then: if a read happens inside an active propagation boundary, it will never see new state. This intuition is however based on Solid 1.0, so take it with a grain of salt.
Escape hatch
I proposed a
withoutBoundaryhelper to Ryan that would unblock propagation while in an active propagation boundary. I currently don't see a need for it anymore, unless the behaviour for "upwards" updates becomes more complex than what I suggested above.Alternative name: "Freeze"
Maybe
PropagationBoundaryis rather technical, and it's better to describe the resulting behaviour rather than the way it is implemented. In that case I'd suggestFreeze, as it "freezes" the contents and any associated effects. Happy to hear your thoughts.