Redirect doesn't early exit component when loading from browser address bar
Duplicates
- [X] I have searched the existing issues
Latest version
- [X] I have tested the latest version
Current behavior 😯
When redirecting inside async server function call, the rest of the component still continue to run inside the server.
routes/admin.tsx
import { cache, createAsync, redirect } from "@solidjs/router";
const isAuthorized = false;
const getUser = cache(async () => {
"use server";
console.log("getUser: before redirect"); // logs
if (!isAuthorized) {
throw redirect("/about");
}
console.log("getUser: after redirect"); // doesn't log
return {
name: "John",
age: 42,
};
}, "getUser");
const Admin = () => {
const res = createAsync(() => getUser());
// server does unnecessary work here when redirecting
console.log("Admin component"); // logs even after redirect, logs twice as well
return (
<div>
<h1>admin</h1>
<p>{res()?.name}</p>
</div>
);
};
Logs in server:
getUser: before redirect
Admin component
Admin component
Expected behavior 🤔
Basically for redirecting to exit early in the component execution, like how NextJS 14 works.
NextJS 14
app/admin/page.tsx
import { redirect } from 'next/navigation';
const isAuthorized = false;
const getUser = async () => {
'use server';
console.log('getUser: before redirect'); // logs
if (!isAuthorized) {
redirect('/about');
}
console.log('getUser: after redirect'); // doesn't log
return {
name: 'John',
age: 42,
};
};
const Admin = async () => {
const res = await getUser();
// server skips rest when redirecting
console.log('Admin component'); // doesn't log
return (
<div>
<h1>admin</h1>
<p>{res?.name}</p>
</div>
);
};
Logs in server:
getUser: before redirect
Steps to reproduce 🕹
Steps:
- clone https://github.com/aquaductape/solid-start-redirect-test
- enter
http://localhost:3000/adminin browser address bar - redirects to
/about - see terminal logs
Context 🔦
No response
Your environment 🌎
System:
OS: macOS 14.4.1 (23E224)
Chip: Apple M1 Pro
Binaries:
Node: v18.17.1
npm: 10.1.0
npmPackages:
solid-start: ^1.0.0-rc.0
Note 1
When navigating to admin page via clicking Admin anchor tag,
it works to my expectation, early exits component after redirect like NextJS
Note 2
If I remove the optional chain operator on res()?.name that's rendered inside Admin component jsx, it crashes on client because res() returns undefined. But if you navigate to admin page via browser addressbar, the crash is ignored (logs error in terminal in dev mode).
const Admin = () => {
const res = createAsync(() => getUser());
// server does unnecessary work here when redirecting
console.log("Admin component"); // logs even after redirect
return (
<div>
<h1>admin</h1>
<p>{res().name}</p>
</div>
);
};
This is by design to a certain degree. Our Async isn't blocking until where you read from the resource, not where you declare it. This avoids waterfalls that Next cannot. Similarly our resources don't throw and can be undefined. Again to be non-blocking. We are exploring space of doing blocking along the reactive graph rather than the components in Solid 2.0. But the short of this is Solid does not work like React and we need to work on our documentation.
The error behavior inconsistency on SSR + Hydration vs navigation is interesting. I'm gathering doesn't cause the server to error for some reason and then it gets skipped during hydration. That is probably worth further examination.
You can avoid the rendering part by not showing the component until the resource is ready.
Add Show as below
const Admin = async () => {
const res = await getUser();
createEffect(()=>{
if (res()) {
// won't log as by the time the component loads and redirects this won't get here
console.log('Client - Admin component');
}
});
// will log, as the resource hasn't loaded to do the redirect yet
console.log('Client and Server - Admin component');
return (
<Show when{res()}>
<div>
<h1>admin</h1>
<p>{res?.name}</p>
</div>
</Show>
);
};
Yeah this by design... reviewing this again I see that the log does happen, just in the browser, on navigation. The reason for the 3 logs is the way SSR works, it fetches (first log), renders the component (second log) and realizes it needs to suspend. Then on completion it renders the component again (realizes it needs to redirect).
In the client it fetches and renders the component (logging the render) then the server receives the response and logs the fetching, and then it resolves without re-running the component as reactivity just continues where it left off.