solidjs / solid-router

A universal router for Solid inspired by Ember and React Router
MIT License
1.11k stars 138 forks source link

<A> not properly triggering nested Suspense #352

Open hamoto opened 6 months ago

hamoto commented 6 months ago

Describe the bug

When using A components for navigation to pages which rely on params or location to render, it is not properly triggering nested Suspense fallbacks. It instead waits until the entire page is rendered off screen and only then updates the page content as well as the browser URL path and sets the activeClass accordingly. I understand that the component has built in transitions and that this may be intended behavior, but this causes applications with slow-loading API requests to feel unresponsive.

I've written a very simple example to recreate the issue:

// @refresh reload
import "./app.css";
import {A, Router, Route, cache} from "@solidjs/router";
import {lazy, Suspense} from "solid-js";

export const resource = cache((foo) => {
    return new Promise(resolve => setTimeout(() => resolve({ foo }), 2000))
}, "test")

export default function App() {
    const loadResource = ({params, location}) => {
        void resource(params.foo)
    }

    return (
        <Router root={(props) => {
            return (
                <>
                    <div className={"flex gap-5"}>
                        <A href={"/test1"} activeClass={"underline"}>Test 1</A>
                        <A href={"/test2"} activeClass={"underline"}>Test 2</A>
                        <A href={"/test3"} activeClass={"underline"}>Test 3</A>
                    </div>
                    <Suspense fallback={"Loading..."}>{props.children}</Suspense>
                </>
            )
        }}>
            <Route path={"/*foo"} component={lazy(() => import("~/Test.jsx"))} load={loadResource}></Route>
        </Router>
    );
}

/*
//Contents of Test.jsx

import {createAsync} from "@solidjs/router";
import {resource} from "~/app.jsx";
import {Suspense} from "solid-js";

export default function Test(props) {
    const testResource = createAsync(() => resource(props.params.foo))

    return (
        <Suspense fallback={"Loading 2..."}><pre>{JSON.stringify(testResource())}</pre></Suspense>
    )
}
 */

Steps to Reproduce the Bug or Issue

Start a new solid-start project, paste the above code into app.jsx, create a new file called Test.jsx in /src and paste the commented out "Contents of Test.jsx" there, run the server and click the Test links

Expected behavior

When the link is clicked, the page URL should update immediately, the clicked link should be assigned its active class ("underline" in this example) and the "Loading 2..." Suspense fallback should be triggered, then 2 seconds later it should be replaced with something like {"foo":"test3"}

Screenshots or Videos

unresponsive_links

Platform

  • OS: Windows
  • Browser: Chrome
  • Version: 0.10.8

Additional context

No response

hamoto commented 6 months ago

Changing the Suspense in Test.jsx to a Show with useIsRouting() corrects the issue:

import {createAsync, useIsRouting} from "@solidjs/router";
import {resource} from "~/app.jsx";
import {Show} from "solid-js";

export default function Test(props) {
    const isRouting = useIsRouting()
    const testResource = createAsync(() => resource(props.params.foo))

    return (
        <Show when={!isRouting()}><pre>{JSON.stringify(testResource())}</pre></Show>
    )
}

this is a viable workaround for the time being.

mshlz commented 6 months ago

I am having the same problem, the suspense fallback is only being activated when I refresh the page itself. None of <A>, <a> or useNavigate triggers it

I made another example to simulate these other cases...

Dependencies:

"@solidjs/router": "0.10.9"
"solid-js": "1.8.11"

"typescript": "5.3.3",
"vite": "5.0.12",
"vite-plugin-solid": "2.8.2"

// test1.tsx, test2.tsx, test3.tsx
export default function TestN () {
    return <h1>TestN</h1>
}
// index.tsx
/* @refresh reload */
import { A, Route, Router, useNavigate } from "@solidjs/router"
import { Suspense, lazy } from "solid-js"
import { render } from "solid-js/web"

const delay = async (ms) => new Promise(resolve => setTimeout(resolve,ms))

// lazy pages
const Test1 = lazy(async () => {
  await delay(1000)
  return import("./test1")
})

const Test2 = lazy(async () => {
  await delay(1000)
  return import("./test2")
})

const Test3 = lazy(async () => {
  await delay(1000)
  return import("./test3")
})

// home page
const Home = () => <h1>Home</h1>

// wrapper with suspense
const AppWrapper = (props) => {
  const navigate = useNavigate()
  return (
    <>
      <nav style={{ display: "flex", gap: "1rem"}}>
        <A href="/test1">Test1</A>
        <a href="/test2">Test2</a>
        <p onclick={() => navigate("/test3")}>Test3</p>
      </nav>
      <hr />
      <Suspense fallback={<h1>Loading...</h1>}>
        {props.children}
      </Suspense>
    </>
  )
}

const dispose = render(() => (
  <Router root={AppWrapper}>
    <Route path="/" component={Home} />
    <Route path="/test1" component={Test1} />
    <Route path="/test2" component={Test2} />
    <Route path="/test3" component={Test3} />
  </Router>
), document.getElementById("root"))

if (import.meta.hot) {
  import.meta.hot.dispose(dispose)
}

EDIT:

Following @hamoto suggestion, I was able to achieve the behavior that I need too (show the Suspense fallback while the lazy import is not completed). In my case I wrapped the {props.children} with the <Show when={!isRouting()}>