-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integrating Remix Router with React Transitions #5763
Comments
We started Remix on Suspense with the experimental releases back in the day but after a couple releases broke things we stopped relying on it to "hold things back" and stuck to the stable React releases. Fast forward and now React Router uses In that world do we still need |
Fair enough! I think everything that's needed here is available in React 18, except for the "throw Promise" thing not being official (which would later become official "use"). But setting that aside, this should be possible to build with stable releases (e.g. it's how Relay works).
At the moment, we can't make Transitions work with external stores. (Hence "sync".) I think we've explored potential APIs for more sophisticated multi-version stores but it's further out. For Transitions to work, the state needs to be "rooted" in React (i.e.
If you'd like Remix users to be able to use Suspense fully (and avoid the issues described above), then yes. I think it's also reasonable that Remix may choose not to support these patterns, and maintain its parallel implementation. I.e. not every React framework has to support the full range of React features. Supporting only a subset of React features makes sense if the framework needs to cater to several UI libraries. Then it would have to use the lowest common denominator and, in the current landscape, it's sync rendering. |
Remix is designed specifically to avoid this kind of UX where render side effects trigger asynchrony, slowing down the page shifting the layout all around the screen as placeholders popcorn in. Would love a concrete example of why you'd want to encourage these waterfalls when Remix has patterns to avoid it completely? I'm quite happy with |
In the above example, it's the Remix version that shifts layout (because you're forced to add a boundary and can't let the content block instead). In the Next example, there is no extra shift because anything that suspends at the top-level "joins" the transition and blocks it. With Next, you have the freedom to decide what should block and what shouldn't. But you decide that on the UI level (with If we set aside implementation concerns and forget about frameworks, I think “stuff outside
Suspending doesn't mean a fetch is necessarily triggered on the client or by rendering. It only means that the component is not ready yet. It could be waiting for something that was triggered earlier and is already being fetched or transferred from the server. (From what I understand, this is how For example, we're working on a CSS integration so that CSS chunks can be sent in parallel with JS (instead of blocking JS execution on them or loading them all upfront). If JS loads before associated CSS, React can start rendering the component. (Yay, no CSS-JS waterfalls!) But React would "wait" for associated CSS to load before revealing the contents of the Suspense boundary whose content depends on that CSS. Images is another example where we might offer an API to "hold back" the boundary so that the route "pops together" with, say, a blurred cover image, or button icons. Another use case is loading code — but it's true Remix is already limited in that sense. From what I understand, Remix can only load code eagerly (bundled with the route) or completely lazily (requested after client render attempt). So there is no way to conditionally prefetch Finally, some things might just have to fetch on the client. I think it would be great if when you're forced to do this, you could still avoid unintentional layout jumps, and could hold back the router a bit. All in all, I don't think this is strictly about one specific pattern. React offers a way to hold the UI back. It's ok not to use it, but I think it will start being more noticeable as more things start integrating it. Both under-the-hood things like CSS, and arbitrary user code taking advantage of this support. |
That makes sense. I've wanted the ability to hold back the router to complete an animation in case the route transition is faster than the animation, or simply avoid flickering a spinner for 10ms. I still maintain that we should be spending our energy designing APIs to avoid render-initiated asynchrony. Async components make it easier to do the wrong thing. |
I feel like the async/await and server components talk is derailing this issue a bit. It doesn't seem exactly related to the discussion of "remix router transitions should be wrapped in |
Yeah sorry, didn’t mean to imply this has something to do with RSC per se. It’s just the latest example of “stuff can suspend anywhere” in my mind because it’s more granular about sending code. But the CSS stuff I mentioned is not gonna be RSC-specific — I think it would actually be great if Remix could try using it too once we get something working for that. I also apologize if the focus on that Next example is distracting — Next isn’t where this behavior is. The heuristic itself is in React 18. (Transitions don’t get committed until you have enough to render the new screen.) So you can imagine the existing useLoaderData API, but if the router is implemented in the Suspensey paradigm (rendering the next route is a transition whose render suspends on its data/code), it should “just work” using the same heuristic. |
Forgive me for intruding on this conversation considering I'm a nobody 😅. @gaearon with the example you gave with the You could certainly wrap it in It would be great if |
What you're describing is what the browser already does natively on anchor tag, full page reload navigation. It makes sense for Remix to mirror this as a default IMO. |
To put something concrete behind this, we've just landed core support for this in the reconciler (facebook/react#26398). No DOM integration yet, but it's coming. This will — at least initially — only be enabled during Transitions. |
Here is what I understood from reading this issue:
|
@Moro-Code for some time you could pass an object with options to https://hu.reactjs.org/docs/concurrent-mode-patterns.html#transitions |
We think the level of control provided now is optimal. Essentially, you have two options:
We might provide a more “global” version of
We do not think a timeout is the right solution. We’ve actually started with that, and removed it. In practice, it’s very hard to pick the value (5 seconds? 10 seconds? 30 seconds?) if it’s developer’s choice. It also feels arbitrary to the user because it causes existing content to disappear (and be replaced with a spinner) for no discernible reason. |
@ryanflorence, is it unlikely that your stance on this issue will change?
I think this is a significant limitation in Remix (and React in general, before Suspense / Transitions). Being able to hold back the rendering of state updates is essential to provide smooth transitions. Both when you add animations to mask fetch delays and the fetch is faster than the animation (the issue I believe you describe?), but also more generally to enable "exit animations" where you keep the old state until an animation is done, before rendering the updated state (sliding content out-of/into view, open/close modals etc.). I agree on your last comment about avoiding render-initiated asynchrony, but I don't think that's necessarily related? |
How are you folks working around this limitation? Our use case is: we have a SPA using React Router, a global suspense boundary, and a page showing some content using a suspense-compatible data fetching library. The request is driven by a search param. Changing the search param triggers a new request, and we were expecting that wrapping the |
|
What version of Remix are you using?
1.14.1
Are all your remix dependencies & dev-dependencies using the same version?
Steps to Reproduce
Remix does not fully integrate with Suspense because the Remix router does not use
startTransition
. I was wondering if there is interest in exploring a fuller integration where Remix router would be more Suspense-y.To illustrate this, I will offer a little example.
Initial setup
Suppose I have an
/index
route that just renders<h1>
and a more complex/about
page that loads some data. My navigation to/about
looks like this:remix_good.mov
The data loads in two stages:
My code for
about.js
looks like this:So far so good.
A wild suspense appears!
Now let's say I add a component that suspends to my
/about
page.Suppose that
Counter
itself suspends for whatever reason. E.g. maybe it renders alazy
component inside. Maybe it does a client fetch with one of the Suspensey libraries. Maybe it is waiting for some CSS chunk to load (when we add Suspensey CSS integration into React). Etc. In general, you can expect components anywhere in the tree to be able to suspend — and that includes the "non-deferred" parts of the UI.At first, we'll have an error:
OK, fair enough, it's a React error. It says there's no
<Suspense>
above it in the tree. Let's fix this.I could add it around my outlet:
But then the whole page "disappears" when it suspends:
remix_flash1.mov
Or I could add it around the
<Counter>
itself:But then my
Counter
"pops in" independently from the<h1>
.remix_flash2.mov
What I really want is to delay the route transition until all content in the "blocking" part (including
<Counter />
and whatever it suspended on) is ready.Is this even possible to fix?
Yes. The canonical fix for this is to wrap the
setState
which caused the navigation intostartTransition
. This will tell React that you don't expect thatsetState
to finish immediately, and so it can keep showing the "previous" UI until the render caused by thatsetState
has enough data to show meaningful UI.As a concrete example, here is a similar navigation implemented in Next.js 13 (App Router):
next_good.mov
Note that although
<Counter />
suspends on the client (with an artificial delay), the router still "waits" for it — it "pops in" together with the<h1>
. I did not need to add any<Suspense>
boundaries — neither around it nor above it.Of course, if I wanted to, I could add a
<Suspense>
boundary around just the<Counter>
. Then it would not "hold" the router from a transition.What's the higher-level point?
Remix currently implements its own machinery to "hold things back" until loaders have returned their result. Then it immediately "commits" the UI — even if that UI might immediately suspend.
React implements a first-class feature to "hold things back" —
startTransition
. (OruseTransition
so you can report progress.) So I am wondering if there is some path towards using the built-in feature.I imagine that, if Remix were to do that, it would start rendering the new route immediately — but it would suspend when in
useLoaderData
. If the routersetState
is wrapped instartTransition
, you'd automatically get the "stay on the previous route until there's enough data to show the next route's shell" behavior. But unlike now, it would be integrated with anything else that suspends, like<Counter />
.Would love to hear any concerns or if there's something missing in React to make this happen. It's exciting Remix is taking advantage of Suspense for React SSR streaming already. Integrating Remix Router with React Transitions seems like a big next step towards Remix users being able to benefit from all React features.
Expected Behavior
Actual Behavior
The text was updated successfully, but these errors were encountered: