Solid router, suspense and navigate

<Router root={<App>{props.children}</App>}>
{routes}
</Router>
<Router root={<App>{props.children}</App>}>
{routes}
</Router>
export default function App(props: { children: JSX.Element }) {
const navigate = useNavigate();
const location = useLocation();
createRenderEffect(() => {
if (location.pathname !== "/login") navigate("/login")
});

return <Suspense>{props.children}</Suspense>
}
export default function App(props: { children: JSX.Element }) {
const navigate = useNavigate();
const location = useLocation();
createRenderEffect(() => {
if (location.pathname !== "/login") navigate("/login")
});

return <Suspense>{props.children}</Suspense>
}
I have 2 routes, let's say /login and /home, the login file is small, loads in 1s, home file is big loads for 10s (because of dependencies). In the case above initially going to /home, it redirects as fast as it can to /login. The /login file is loaded, but suspense still holds until the /home file is loaded, then it shows /login. How can I make it show /login just as soon as it loaded, not waiting for /home ? (not solid start) (Tried it in production build)
12 Replies
TechAlchmy
TechAlchmy6d ago
One approach to achieve this in Solid.js is to adjust your App component to conditionally render components based on the loading state of your routes.
Maciek50322
Maciek50322OP6d ago
How to do that? How can I get loading state of route? And what do you mean about conditionally rendering components?
peerreynders
peerreynders6d ago
I'd be inclined to try:
// authenticated is an accessor
return (
<Show when={authenticated()} fallback={<Navigate href="/login" />}>
<Suspense>{props.children}</Suspense>
</Show>
);
// authenticated is an accessor
return (
<Show when={authenticated()} fallback={<Navigate href="/login" />}>
<Suspense>{props.children}</Suspense>
</Show>
);
https://docs.solidjs.com/solid-router/reference/components/navigate nevermind won't work with /login
Navigate - SolidDocs
Documentation for SolidJS, the signals-powered UI framework
peerreynders
peerreynders6d ago
The solid start example can work with plain vanilla@solidjs/router — you just have to replace the "use server" sections with your style of authentication. - have a query value dedicated to holding the user/account information or undefined - on unauthenticated access throw redirect
redirect - SolidDocs
Documentation for SolidJS, the signals-powered UI framework
GitHub
solid-start/examples/with-auth/src/lib/index.ts at f83526c45a65af6a...
SolidStart, the Solid app framework. Contribute to solidjs/solid-start development by creating an account on GitHub.
Maciek50322
Maciek50322OP6d ago
I'll check it out later, but point of this question wasn't authentication, but rather about suspense behaviour when loading bigger files with instant redirection
peerreynders
peerreynders6d ago
I'm thinking that you can tweak that by organizing the (route) layouts and preloads.
peerreynders
peerreynders6d ago
In Strello the user query will force the redirect. Meanwhile the root layout common across all routes deliberately accesses the user query forcing the redirect to /login (while itself remaining in place) when there is no authentication. It's a matter of your implementation of getUser() - being run as quickly as possible - quickly deciding to throw the redirect.
GitHub
strello/src/components/Layout.tsx at 9c9ae973d96cc045914e696757a1b5...
Contribute to solidjs-community/strello development by creating an account on GitHub.
GitHub
strello/src/lib/index.ts at 9c9ae973d96cc045914e696757a1b5f31efc6fa...
Contribute to solidjs-community/strello development by creating an account on GitHub.
peerreynders
peerreynders6d ago
Sketch:
function App(props: { children: JSX.Element }) {
const location = useLocation();
createRenderEffect(() => {
if (location.pathname !== '/login') navigate('/login');
});

return <Suspense>{props.children}</Suspense>;
}

function Protected(props: RouteSectionProps) {
const account = createAsync(() => getAccount());
return <Show when={account()}>{props.children}</Show>;
}

function Login(props: RouteSectionProps) {}

function Home(props: RouteSectionProps) {}

<Router root={<App>{props.children}</App>}>
<Route path="/" component={Protected}>
<Route path="/" component={Home} />
</Route>
<Route path="/login" component={Login} />
</Router>
function App(props: { children: JSX.Element }) {
const location = useLocation();
createRenderEffect(() => {
if (location.pathname !== '/login') navigate('/login');
});

return <Suspense>{props.children}</Suspense>;
}

function Protected(props: RouteSectionProps) {
const account = createAsync(() => getAccount());
return <Show when={account()}>{props.children}</Show>;
}

function Login(props: RouteSectionProps) {}

function Home(props: RouteSectionProps) {}

<Router root={<App>{props.children}</App>}>
<Route path="/" component={Protected}>
<Route path="/" component={Home} />
</Route>
<Route path="/login" component={Login} />
</Router>
Maciek50322
Maciek50322OP6d ago
I think the Show can do someting but I'll try it when I get back Yeah so Show doesn't actually change anything... unless I do so
export default function App(props: { children: JSX.Element }) {
const navigate = useNavigate();
const location = useLocation();
createRenderEffect(() => {
if (location.pathname !== "/login") navigate("/login")
});

const [show, setShow] = createSignal(false);
onMount(() => {
setTimeout(() => {
setShow(true);
});
});

return (
<Show when={show()}>
<Suspense>{props.children}</Suspense>
</Show>
);
}
export default function App(props: { children: JSX.Element }) {
const navigate = useNavigate();
const location = useLocation();
createRenderEffect(() => {
if (location.pathname !== "/login") navigate("/login")
});

const [show, setShow] = createSignal(false);
onMount(() => {
setTimeout(() => {
setShow(true);
});
});

return (
<Show when={show()}>
<Suspense>{props.children}</Suspense>
</Show>
);
}
This code above achieves what I want. It doesn't even fetches /home, so just shows /login when it loads. I like this, I can prefetch /home on my own. Without setTimeout it doesn't work. I don't like that I have to do this with setTimeout, because I'm not sure if this behavior will be consistent. Your solution with createAsync probably will work too, because getting account can take some time. But going back: authorization is not point of this question. What I mean is to force suspense to resolve when latest route is loaded, not all previously requested. To visualize: We have suspense on props.children, which is current route component.
<Suspense>{props.children}</Suspense>
<Suspense>{props.children}</Suspense>
User first clicks button to go to /about, this page is big, it takes 10s before it loads. So in first second of waiting, user gets bored by waiting and clicks button that navigates to /home, which is very small, it takes 1s before it's loaded OR maybe it's even prefetched, so it takes no time to load it.
<>
<Button onClick={() => navigate("/about")}>
About
</Button>
<Button onClick={() => navigate("/home")}>
Home
</Button>
</>
<>
<Button onClick={() => navigate("/about")}>
About
</Button>
<Button onClick={() => navigate("/home")}>
Home
</Button>
</>
What actually happens is user is waiting for 9 more seconds, until he is redirected to /home, even though it's been loaded this whole time. User changed his mind and doesn't want to go to /about, he wants to go to /home, but has to wait until/about is loaded. The /about page is loading for 10s not because of asyncs or resources, but because it's size is big and network takes this time to fetch the js file and might have only static content
zulu
zulu5d ago
the setTimeout bypass the transition it will work as long as what you are doing is working now
peerreynders
peerreynders5d ago
What I mean is to force suspense to resolve when latest route is loaded, not all previously requested.
The navigate isn't issued until the render. By that point in time the home async operations have already been triggered and as such committed to. Given that you have one common Suspense boundary the login async ops are just added to the same suspense queue and suspense does not complete until all ops have settled. So one way to approach this is to ensure that both home and login have separate, non-overlapping Suspense boundaries. That way when you flip over to the login render, the home async ops aren't already queued up on the suspense boundary. The Show approach on the other hand simply ensures that the async ops are not committed to until it ensures that some precondition is met. In the context of the example the most obvious precondition for committing to loading home is “are we authenticated yet”. It just seems more relevant than implementing a separate “is this the initial render” mechanism.
Maciek50322
Maciek50322OP5d ago
Okay, now it's clear, after removing suspense from <App> and putting it deeper - in each page - it works as expected I avoided doing this Protected template, because I use vite-plugin-pages and because there is no unnamed route group, this would look like /protected/home. Tho I think this can be configured there with dirs setting, but still, having routes represented in folder / file names with exceptions written out in different settings file would make it more confusing. So I think I'll switch to component routes and use this. or tankstack router hmmm but this issue is solved so thanks nvm can make my own consistent rules with vite-plugin-pages and onRoutesGenerated

Did you find this page helpful?