vikejs / vike-react

🔨 React integration for Vike
https://vike.dev/vike-react
MIT License
94 stars 16 forks source link

Improvements around page transition animations #117

Open brillout opened 2 months ago

brillout commented 2 months ago

Defer next page + loading bar

A neat page transition is to preserve the current page and defer showing the next page until it's fully loaded, while showing a loading bar (like YouTube or nprogress). I think it's already possible for the user to implement this?

How about we make this the default behavior?

Default

Currently, vike-react doesn't provide any default page transition strategy, but I think it should. (While enabling the user to override this default.)

<Suspense>

How about pages that have suspense? (More precisely: when the next page has one or multiple <Suspense> boundaries.) In particular when using vike-react-query and vike-react-apollo.

With @nitedani we already have ideas about this (withFallback() and a new setting +Loading.js).

Main <Suspense>

How about this: let the user declare the "main" <Suspense> boundary. As long as this main suspense isn't resolved then keep showing the old page with a loading bar. As soon as this main suspense is resolved, then show the next page (consequently showing all suspense boundaries that are still loading).

For example, a product page would start to be shown to the user upon page navigation only after the product content of the page is fetched from the database.

In other words, it's blocking the rendering the next page. API example:

import { useSuspenseQuery } from '@tanstack/react-query'
import { await } from 'vike-react-query'

const Movie = await(
  ({ id }: { id: string }) => {
    const result = useSuspenseQuery({
      queryKey: ['movie', id],
      queryFn: () =>
        fetch(`https://brillout.github.io/star-wars/api/films/${id}.json`)
        .then((res) => res.json())
    })
    const { title } = result.data
    return (
      <div>
        Title: <b>{title}</b>
      </div>
    )
  }
)

There could be several await() components for a single page. (Although, in the (vast?) majority of cases, the user would likely define only one await() component.)

I ain't sure how an implemention would look like, but IIRC React 18/17 introduced a new feature that was addressing this use case. (Start rendering the next page in a seperate virtual DOM while preserving the current page.)

No <Suspense>

What should happen when there isn't any <Suspense> boundary? (Which is also possible with vike-react-{query,apollo} when the user doesn't use useFallback().) I guess this is the default behavior, i.e. defer showing the next page + loading bar.

See also

UROjQ6r80p commented 2 months ago

I've created small minimum repo containing example of implementation without vike-react: https://github.com/UROjQ6r80p/vike-suspense-loading-ui

At first I thought either useTransition or useDefferedValue is required but it seems it's unneccessary and might lead to subtle bugs. It seems that Vike by default stays on the current page for suspense based hooks (useAsync for example) IF there is no Suspense wrapper. That's why I think it can be created based on conditional SuspenseWrapper (similar to how Layout and Wrapper are currently implemented in vike-react).

I suggest +loading.jsx naming convention just like next.js does (https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming), or maybe +fallback.jsx?

and so if there is +loading.jsx setting in the folder, Suspense with fallback from +loading.jsx should be returned, or empty PassThrough component.

The catch is that ideally for pages without +loading.jsx (because creating tailored skeleton requires more effort and might not be desirable on every page) I suppose most people would like to use something like ngprogress to display loading indicator on top just like youtube/github and thousands of other sites do.

if a user wants to implement loading indicator with for example useAsync() then onPageTransitionStart works without problems, but onPageTransitionEnd does not wait for suspense based hooks, it fires immediately. It can be easily mitigated by custom HOC wrapping Page like in my example (https://github.com/UROjQ6r80p/vike-suspense-loading-ui/blob/main/renderer/withTransitionEnd.jsx) or another meta setting. But I think adding separate meta setting (something like onSuspenseTransititionEnd for it is not ideal and could be confusing.

so my question to maintainers is, can onPageTransitionEnd itself be changed so that it would wait for suspense based hooks just like it waits for +data.js?

brillout commented 2 months ago

so my question to maintainers is, can onPageTransitionEnd itself be changed so that it would wait for suspense based hooks just like it waits for +data.js?

I was also thinking that. Vike actually already awaits onRenderClient() and after the promise resolves Vike calls onPageTransitionEnd(). So all we need to do is to make onRenderClient() await the transition.

nitedani commented 1 month ago

It seems that Vike by default stays on the current page for suspense based hooks (useAsync for example) IF there is no Suspense wrapper.

Why is this the case? Why is useTransition not required? I this is true, it would mean that the loading bar would behave correctly, on pages with or without suspense.

Page with Suspense -> show the suspense fallback Page without Suspense-> show the loading bar

Isn't this the desired behavior? Why do we want to show the loading bar on a page that has suspense?

brillout commented 1 month ago

Why is this the case?

So it seems that, if there isn't any <Suspense> boundary, then React first renders everything to its virtual DOM and flushes it to the real DOM in one batch. I think that makes sense.

Page with Suspense -> show the suspense fallback
Page without Suspense-> show the loading bar

Isn't this the desired behavior? Why do we want to show the loading bar on a page that has suspense?

Yea, if the page has suspense then I also think that we don't need to show the loading bar.

The only exception being Main <Suspense>. In that case, the loading bar is shown until all await() components are resolved.

UROjQ6r80p commented 2 weeks ago

It seems that Vike by default stays on the current page for suspense based hooks (useAsync for example) IF there is no Suspense wrapper.

I checked it again and I think it works only in dev mode, after everything is built and run in prod it does throw errror after client side navigation to the page with useAsync hook: A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition.

I have tried it with my own example, and also vike-react, so if I want to use useAsync then there has to be <Suspense> wrapper it seems (only in production, in dev it works fine)