You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: src/blog/tanstack-router-navigation-lifetimes.md
+9-15Lines changed: 9 additions & 15 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -37,7 +37,7 @@ Account loading, the redirect to login, caching, and rendering proceed on differ
37
37
</figcaption>
38
38
</figure>
39
39
40
-
There is a lot to explain here. Why can `/account` keep loading after the user moves on, but no longer update the page? Why does the `/settings` error never appear? And why isn't publishing `/login` the end of the navigation?
40
+
Why can `/account` keep loading after the user moves on, but no longer update the page? Why does the `/settings` error never appear? And why isn't publishing `/login` the end of the navigation?
41
41
42
42
**A navigation looks like one asynchronous operation, but the router must decide four things independently: which loading work should continue, which navigation may publish, what the route attempt decided, and whether the framework rendered it.**
43
43
@@ -71,7 +71,7 @@ The **lane** in the middle is a private, unpublished draft of the matched route
71
71
> [!NOTE]
72
72
> The diagram's `release flight` label marks where the lane releases its temporary claim. That does not necessarily end ownership of settled data. The loader-flight section below explains why.
73
73
74
-
Nothing goes wrong in this version. The transaction is never replaced, the loaders do not error or redirect, and both publications render successfully. The interesting cases appear when complications happen:
74
+
Nothing goes wrong in this version. The transaction is never replaced, the loaders do not error or redirect, and both publications render successfully. Now change one assumption at a time:
@@ -80,21 +80,19 @@ Nothing goes wrong in this version. The transaction is never replaced, the loade
80
80
| parallel loaders produce different kinds of outcomes? | A [**private lane**](#one-loader-result-is-not-the-route-result)|
81
81
| another publication takes over before the framework renders this one? | A [**framework render receipt**](#published-does-not-mean-rendered)|
82
82
83
-
Usually, all four answers arrive within a few milliseconds of each other. That is what preserves the useful illusion of one asynchronous task. It is also plenty of time for another event to change what should happen next.
83
+
Usually, all four answers arrive within a few milliseconds of each other, preserving the illusion of one asynchronous task. It is also plenty of time for another event to change what should happen next.
84
84
85
85
## When Navigations Overlap
86
86
87
-
The opening scenario puts all four complications back together. The next diagram gives us a map of the whole thing. It is dense, but the four sections after walk through each important part.
87
+
The opening scenario puts all four complications back together. The next diagram maps them before the following sections examine each one.
88
88
89
89
Unlike the other diagrams, this one reads from **top to bottom**. Each column follows one owner: the current transaction, a private lane, a loader flight, or the framework render. The horizontal arrows pass work or results between them. This is only one possible interleaving; independent events, such as caching `/account` and publishing `/login`, could happen in either order.
90
90
91
91
<imgsrc="/blog-assets/tanstack-router-loading-lifetimes/nav-orchestra_concurrent-orchestration.svg"style="width:100%; max-width: 600px; margin: auto;"alt="A detailed sequence diagram where an account preload and navigation share a loader flight, settings supersedes account, nested settings loaders produce an error and redirect, account data reaches cache, login matches publish, and the framework acknowledges rendering them">
92
92
93
-
Let's take those boundaries one at a time.
94
-
95
93
### A Replaced Navigation Can Leave Useful Work <!-- "lease" explainer -->
96
94
97
-
The first surprise in the opening scenario is that`/account` keeps loading after the user clicks away. Both the hover preload and the navigation need `/account`'s data, but the `loader` should still run only once.
95
+
In the opening scenario,`/account` keeps loading after the user clicks away. Both the hover preload and the navigation need `/account`'s data, but the `loader` should still run only once.
98
96
99
97
The preload and navigation still do their own matching, build their own context, and run their own `beforeLoad`. They share only the loader invocation and its outcome. Reusing work must not mean inheriting another consumer's draft of the page.
100
98
@@ -109,7 +107,7 @@ Consumers acquire and release their own claims on one loader invocation. The fli
109
107
110
108
For `/account`, clicking `/settings` ends the navigation's claim. The preload still holds its own lease, so the count never reaches zero. The flight continues, settles, and its result enters the cache.
111
109
112
-
This is the key separation: losing permission to publish `/account` does not prove that its loader is useless. The transaction enforces whether `/account` may publish, and the flight's leases tracks whether the loader is still needed.
110
+
Losing permission to publish `/account` does not prove that its loader is useless. The transaction enforces whether `/account` may publish, and the flight's leases track whether the loader is still needed.
113
111
114
112
> [!NOTE]
115
113
> Preload and navigation lanes, published routes, and cache entries can all hold leases. A lease can also outlive the promise it covers: the lease represents ownership of the resource, not a promise lifecycle.
@@ -129,8 +127,6 @@ At each async boundary, the lane compares its transaction with the current slot.
129
127
130
128
Discarding the navigation also releases its leases. Any pending flight with no other consumers may now abort. Our `/account` flight has the preload consumer, though, so it carries on.
131
129
132
-
This is the simple role of the current transaction, to controls which navigation may publish, not how long shared loader work remains useful.
133
-
134
130
> [!NOTE]
135
131
> The implementation check is deliberately small. At an asynchronous boundary, a lane that no longer holds the slot cleans up and returns instead of publishing:
136
132
>
@@ -144,7 +140,7 @@ This is the simple role of the current transaction, to controls which navigation
144
140
145
141
### One Loader Result Is Not the Route Result <!-- "lane" explainer -->
146
142
147
-
In the successful navigation, every loader contributes to one successful result. But of course it can get more complicated. The diagram below shows three loaders: the root route succeeds, the parent layout loader rejects, and the child index loader throws `redirect('/login')`.
143
+
In the successful navigation, every loader contributes to one successful result. The diagram below shows three outcomes: the root route succeeds, the parent layout loader rejects, and the child index loader throws `redirect('/login')`.
148
144
149
145
Each settlement tells us what happened to one loader, but not yet what the whole route attempt should do. The outcomes go back to the private lane, which **reduces** them into one result.[^reduction]
150
146
@@ -168,9 +164,7 @@ It is tempting to say that the redirect "beats" the error, but that is not quite
168
164
169
165
### Published Does Not Mean Rendered <!-- "ack" explainer -->
170
166
171
-
In both scenarios, we're represented the end of a successful lane as a two-step process: publish and ack.
172
-
173
-
Publishing means handing a route branch to the framework. It is a request to render, not proof that the framework committed that branch.[^publication-events]
167
+
Both scenarios represent the end of a successful lane as two separate events: publish and acknowledge. _Publishing_ means handing a route branch to the framework. It is a request to render, not proof that the framework committed that branch.[^publication-events]
174
168
175
169
React may still be busy with the previous tree. The new one can suspend on promises of its own (`useSuspenseQuery`, `use(promise)`, `lazy(() =>import(...))`, etc.), leaving the committed UI on screen. If another navigation publishes in the meantime, the earlier publication may never commit at all.
176
170
@@ -214,7 +208,7 @@ The four colors summarize the independent answers that carry one `navigate()` ca
214
208
</figcaption>
215
209
</figure>
216
210
217
-
Now we have a simple, robust`navigate(...)`.
211
+
Now we have a simple `navigate(...)`.
218
212
219
213
> [!NOTE]
220
214
> These four lifetimes are a teaching slice, not a complete inventory. The implementation also separates preflight planning, pending UI presentation, preload and cache entries, hydration handoff, development HMR rollback, server request cleanup, and stream ownership. Background reloads keep successful loader data visible while a private candidate runs, then require both their transaction and exact committed base to remain current before publishing.
0 commit comments