# Run-Once Suspense
While a lot of the ideas I've been proposing seem pretty powerful there is definitely some complications and reasons for resistance. Truthfully they already exist in current models, but perhaps this is our chance to do something about it. So I have a fairly radical idea from an academic standpoint, but possibly the right tradeoff from the practical sense. So I'm going to explore it here.
### Goals
1. Provide a colorless (non-nullable) async model.
2. Restore confidence in using Suspense by leaving more control in the developers hands.
3. Have a model that is consistent across SSR and client updates
### Sub-Goals
1. Have the default behavior still be reasonable for people unaware of these features.
2. Remove the need for `latest` style APIs and the challenges that come from introducing back in nullable async.
## Concept
Instead of seeking consistency in all things we take a default of:
*Hold on Creation, Tear on Update*
While an odd position to take as someone seeking consistency some conversations with Dan Abramov a year or so back were pretty illuminating to me but I didn't really actualize it until now. He told me while Transitions were good he expected most developers to opt into tearing to escape Suspense. This is interesting because async tearing is the default for frameworks, but now with Suspense you needed to jump through hoops like `useDeferredValue` in React or `.latest` in Solid.
We have to consider the whole impetus behind the Transition API is that once you have loaded content you rarely want to go back to fallback. And yet it is what happens naturally the second you update data as soon as you add Suspense. It is simple experience for the naive API but in real apps it is pretty clear that people pretty much never want stuff being ripped out from under them on an update.
A small but important development was a later change I believe Ricky made to Suspense which was new Suspense boundaries always went to fallback in a Transition while existing one participated in the Transition. It was a very clever approach to seperate creation from update. As developers could control behavior simply by how they structured their code. We implemented this as well. But more so it very much sets a precedence for what I'm proposing today.
Final inspiration for this was a comment from chat from my stream that said `post.latest || post()`. I'm unsure if the poster realized the impact this would have on my thinking but innately this is what most developers would think they'd want most of the time. It is tearing when a value has been set while holding if it isn't. While its hard to have the full context of reasoning behind these sort of decisions. I admit I haven't explored this as the primary model so lets do it.
## Proposal
High level this proposal is top-level holding, explicit Transactions, and "run-once" Suspense boundaries.
Top-level holding because with colorless async we can't do predictable tearing and it aligns with SSR. This will push everyone to Suspense which is what we want. But it won't tie their hands because by default Suspense will only go to fallback on creation. We need that for SSR but we don't need it afterwards. Updating async data will tear rather than go to fallback by default. From here explicit Transactions are still valuable for big changes. Things like routing but most people won't use them directly. Or have to worry about Suspense boundaries triggering again.
Specifically, I'm thinking of following these rules:
1. In absense of Suspense any thrown promises hold all rendering.
2. Under normal operation only async values that have never been resolved throw. Once they have been resolved once they read from latest. With one exception, if they are read under a newly created Suspense boundary.
3. All async reads will register with Transactions unless they are under a newly created Suspense boundary.
## Examples
### ~~Three~~ Two modes on Initial Render
Let's bring back my example:
```jsx
function Post(props) {
const post = createAsync(() => getPost(props.id))
return <>
<Nav selected={props.id} />
<h1>{post().title}</h1>
<p>{post().body}</p>
</>
}
```
By default this would show nothing the post has loaded. We would hold all effects until the async is done. It may not be expected but this removes inconsistency. We can't control tearing under initial conditions.
As before adding Suspense would allow us to show the Nav immediately and then wait to show the post body.
```jsx
function Post(props) {
const post = createAsync(() => getPost(props.id))
return <>
<Nav selected={props.id} />
<Suspense fallback="Loading Post...">
<h1>{post().title}</h1>
<p>{post().body}</p>
</Suspense>
</>
}
```
What about tearing? I'm proposing you can't using Async primitives. Not only does that not work with SSR its rarely desirable initially. You probably would use tearing to show loading states initially and we have Suspense for that. More so it leaves it open for people to experience `undefined` values. If you can render part of the UI before the data it depends on is present it could lead to needing extra checks to prevent interactions. It makes TypeScript challenging to handle those `undefined`s etc...
What if you really want to tear on initial render. Use `createEffect` or `onMount` instead. In a sense then you are pulling it outside of initial render. The server doesn't see it during SSR so it because purely client side and that's that really.
```jsx
function Post(props) {
const [post, setPost] = createSignal();
createEffect(
() => props.id,
id => { fetchPost(id).then(setPost) }
)
return <>
<Nav selected={props.id} />
<h1>{post()?.title}</h1>
<p>{post()?.body}</p>
</>
}
```
### ~~Three~~ Two Modes on Update
Now the proposal is to tear on update by default because existing resources that have resolved once resolve from latest. So are starter code here works probably as you'd expect.
```jsx
function Post(props) {
const post = createAsync(() => getPost(props.id))
return <>
<Nav selected={props.id} />
<h1>{post().title}</h1>
<p>{post().body}</p>
</>
}
```
When `props.id` changes the `Nav` will update immediately and then the post will when it comes in.
Similarly having Suspense here doesn't change the behavior.
```jsx
function Post(props) {
const post = createAsync(() => getPost(props.id))
return <>
<Nav selected={props.id} />
<Suspense fallback="Loading Post...">
<h1>{post().title}</h1>
<p>{post().body}</p>
</Suspense>
</>
}
```
Because both the Async resource and the Suspense have resolved before they won't trigger Suspense and we get tearing.
Now you might want to show a loading indicator when its tearing. We could have the Nav draw a spinner by passing it through using a `isLoading` helper. Interestingly because we hold on creation/top-level this would never appear initially only after the fact.
```jsx
function Post(props) {
const post = createAsync(() => getPost(props.id))
return <>
<Nav
selected={props.id}
loading={isLoading(post)}
/>
<Suspense fallback="Loading Post...">
<h1>{post().title}</h1>
<p>{post().body}</p>
</Suspense>
</>
}
```
It might seem weird to have 2 different loading affordances but one is a placeholder and the other is a loading indicator. In a sense `Suspense` probably would make more sense to be named `Placeholder`. It isn't for handling all loading states, just "initial" ones.
If you really wanted your loading indicator to be a placeholder here you could nest a `Show` I suppose.
```jsx
function Post(props) {
const post = createAsync(() => getPost(props.id))
return <>
<Nav selected={props.id} />
<Suspense fallback="Loading Post...">
<Show
when={!isLoading(post)}
fallback="Loading Post..."
>
<h1>{post().title}</h1>
<p>{post().body}</p>
</Show>
</Suspense>
</>
}
```
There is duplication here but basically Placeholder mode isn't really the intended mode for updates. There are exceptions I will cover in the next secion but for the most part in the same way it is difficult to tear on initial creation it is more onerous to placeholder on update.
The last mode is holding here and that can be handled via Transaction. In this case our component stays the same:
```jsx
function Post(props) {
const post = createAsync(() => getPost(props.id))
return <>
<Nav selected={props.id} />
<h1>{post().title}</h1>
<p>{post().body}</p>
</>
}
```
But our action that updates props.id... maybe from the router wraps that update. Just to show the APIs in action you can picture something like this.
```jsx
function App() {
const [postId, setPostId] = createSignal(1);
const [isPending, start] = useTransaction();
return <>
<Post id={postId()} />
<button onClick={() =>
start(() => setPostId(id => id + 1))
}>
Next Post
</button>
</>
}
```
Most of the time you won't be writing your own Transactions but certain updates in the system will participate and this ensures that all their changes apply at the same time (similar to initial render) but they won't be blocking so your app stays responsive.
### Simple Routing
So far we've considered the basic case. And the takeaway should be that the happy path is more or less the original code we wrote with Suspense + a loading indicator. It won't interfere with normal operation going to fallbacks unexpectedly, it can participate in Transactions without requiring your code to change. It sets you up with a pattern that works universally for SSR. And the async is all non-nullable.
It pushes you into basically only 2 of the 3 modes for each phase:
**Creation:** Hold(default), Placeholder(Suspense)
**Update:** Tear(default), Hold(Transaction)
But are the creation/update boundaries so clear? For the most part I think yes. Start with router/tab example:
```jsx
function App() {
const [tab, setTab] = createSignal("A");
return <>
<Nav selected={tab()} setTab={setTab} />
<Suspense fallback="Loading Page...">
<Switch>
<Match when={tab() === "A"}><A /></Match>
<Match when={tab() === "B"}><B /></Match>
<Match when={tab() === "C"}><C /></Match>
</Switch>
</Suspense>
</>
}
```
Now I've taken the liberty of adding a Suspense boundary above the `Switch`. I'm going to sassume these components might be `lazy` and without a Suspense Boundary here we wouldn't show the `Nav` or anything until we went and grabbed the component code. Moreover without that Suspense boundary as we switched tabs we would freeze our app while navigating. It would not update anything in the display until the lazy code loaded and rendered.
While it you might think technically this is an update, the `lazy` component is a new Async primitive that hasn't resolved before. So adding Suspense also means that as we navigate we go to fallback while the `lazy` component loads. If we navigate back to the same page a second time it won't go to fallback over the `lazy` component but the first time we hit it (unless we preloaded ...maybe on hover) it will.
So Suspense does what you expect. You will see a Placeholder when you navigate. If you forgot Suspense you won't see a placeholder but you've invented a glorified MPA. Click the link and then wait while you can't do anything and the current page locks up.
And this applies to new `createAsync` primitives you might find in `<A />` or any of the pages. Since those will be new async primitives if they are read directly they will Trigger the same Suspense boundary on navigation. Since unlike `lazy` these are created everytime you navigate to the page they would trigger the fallback like you'd expect on new navigation.
However once on the page those Async primitives would never trigger that Suspense again. Updating data wouldn't do it. This means you retain full control within your page.
This not triggering fallback also includes updating search or query params. So I imagine most routers would opt into Transactions as holding semantics are preferred over tearing or Placeholders on future navigations. Since the Suspense Boundary is above the `Switch` it is already exists at the time of navigation so wrapping the tab switch be a Transaction would mean that it could hold without blocking when it does its updates. But it allows you to still tear on yours.
### Advanced Routing
The first scenario is what if you want placeholders still when there are Transactions or to show parts of your page immediately or sooner than other parts because it is ok that they come in later. The answer to both of these is nested Suspense. As a new Suspense boundary it will not participate in the Transaction and go to fallback instead, and it can catch your Async reads without trickling up to the parent Suspense boundary.
```jsx
function A() {
const data1 = createAsync(() => fetchData1());
const data2 = createAsync(() => fetchData2());
return <>
<h1>Important Data</h1>
{data1()}
<Suspense fallback="Loading Slow Less Important Data...">
{data2()}
</Suspense>
</>
}
```
You could wrap all the data reads in nested Suspense here and if the component wasn't `lazy` or had already loaded the part outside of the `Suspense` will render immediately instead of holding or showing the outer fallback. This level of control should be enough for most things. And more importantly updating either `data1` or `data2` after the fact won't cause any Suspense boundary to trigger so you don't need to worry about having some impact of adding them outside of initial creation time.
The second scenario is what if the `createAsync` lives in Context that exists above the router. Then this is not a new Async primitive:
```jsx
function A() {
const asyncData = useContext(AsyncDataContext)
return <>
<h1>Page A</h1>
{asyncData()}
</>
}
```
Following our basic rules this will assume that you want to read the previous value while loading even if through navigating you triggered loading new data. The Suspense and the `createAsync` are already existing. So you will probably navigate to the page immediately and then see old data flicker before the new data comes in. Not a great experience.
Now if that update is downstream from a Transaction there are no problems. It will hold the previous value until it is ready to apply all updates. So stuff related to most routers I imagine will never experience this.
Outside of Transactions my proposed solution to this (and the reason for the caveat in rule #2 above) is if you want to show a Placeholder instead of having the weird Tear flicker... use nested Suspense. A new Suspense boundary will Suspend on an existing Asynnc resource being in a pending state.
```jsx
function A() {
const asyncData = useContext(AsyncDataContext)
return <Suspense fallback="Loading Page A...">
<h1>Page A</h1>
{asyncData()}
</Suspense>
}
```
This is a bit of an exception of the rules but the assumption is any new Suspense boundary expects what it reads to be consolidated before it shows anything. It would be weird to have it show and then update a moment later when it knows it will update.
## Conclusion
Well that is just a few simple examples but I think this model could be viable. The biggest change is getting used to the blocking hold top level behavior. But it solves a lot of problems in terms of unpredictable tearing and aligning SSR with client.
I think the key to this is introducing `createAsync` and `lazy` together with `Suspense`. Until you use either of those you will not hit these issues. However when you get past that you no longer need to immediately learn how to not trigger Suspense. You can adopt those 3 primitives as you see fit without impacting your current code. And hopefully it will encourage people to use `Suspense` more.
If successful then `Transactions` can be a more advanced topic. Truthfully this still doesn't alleviate the why the Router works the way it does without further explanation but it won't impact the localized view of your code. There is no `latest`. No undefined async. At most there is likely a `isLoading` helper which can be used against any potentially async expression/dependency graph to show loading indicators.
I think the trickiest thing might be how to reconcile local resource loading with Transactions. We might see a lot of like `loading={isRouting() || isLoading(post)}`. That needs a bit more exploration but maybe this proposal otherwise hits all the right spots.