S
SolidJS6mo ago
florian

Run route.load and access return value only once when navigating to page

Hey, I am working on a route where before rendering the route, basic middleware needs to run to check if the client should have access to the route by checking the query parameters of the url and redirect if not. During this check I need to decrypt a query parameter, which can only be done on the server as I don't wanna share my secret key used to decrypt it with the client but I also would like to pass parts of the decrypted data to the route component to render on the page. Here is a small example:
const getEmail = () => {
"use server";
const query = getQuery();
if (query.state) {
try {
const state = auth.decryptPayload(query.state);
if (state.email && state.code) {
return state.email;
} else {
throw new Error();
}
} catch {
throw redirect("/login");
}
} else {
return redirect("/login");
}
};

export const route = {
load: () => getEmail()
};

export default function Verify() {
const email = createAsync(() => getEmail());

return (
<div>email()</div>
);
}
const getEmail = () => {
"use server";
const query = getQuery();
if (query.state) {
try {
const state = auth.decryptPayload(query.state);
if (state.email && state.code) {
return state.email;
} else {
throw new Error();
}
} catch {
throw redirect("/login");
}
} else {
return redirect("/login");
}
};

export const route = {
load: () => getEmail()
};

export default function Verify() {
const email = createAsync(() => getEmail());

return (
<div>email()</div>
);
}
To do this I would like the route.load function to run getEmail once to do the middleware checks and decrypt the data to return parts of it to the page component so that the server function getEmail only needs to run once when directly loading the page (and directly render the email without calling the server function once the page has loaded on the client) or when navigating to the page from a different route (only run server function). How can I do this with solid start? Right now it loads the page and the email isn't rendering at all as it seems like the query parameters are empty when getQuery is called for some reason? Also, the server function returns a redirect to /login, as it should when the query parameters don't match but it doesn't navigate to /login, I guess because its a status code 200 response instead of 3xx.
No description
10 Replies
Brendonovich
Brendonovich6mo ago
Using redirect requires wrapping the function with cache or action, in this case you'll want to use cache getQuery will only contain the values you need when the page is being server rendered When getQuery is being invoked by the client during a client navigation you won't have the query params included with the request You probably want to take the state as an argument to the function and then read the query parameters from the router and pass them in
peerreynders
peerreynders6mo ago
FYI: A route load function is passed a { params, location, intent } argument. location.search contains the query string which you could then pass to a server function as an argument. Server side you can feed that string into URLSearchParams for easier access. The other subtlety is that the redirect won't happen when the load is run; it can't because load may run for an opportunistic preload. The redirect happens the moment a component tries to consume the cache that “experienced” the redirect with a createAsync().
MDN Web Docs
URLSearchParams: URLSearchParams() constructor - Web APIs | MDN
The URLSearchParams() constructor creates and returns a new URLSearchParams object.
florian
florianOP6mo ago
Thanks! I updated my code based on both of you guys answers and now it writes the email into the returned html when directly loading the page and when navigating to it on the client so it seems like this issue got solved. I noticed that the server function is running twice, doesn't matter if its client side navigation to the page or directly loading the page. Its running once when navigating/rendering and once after for some reason. Would it be possible to prevent this somehow? Here is the updated example:
const getEmail = cache(async (search) => {
"use server";
const query = new URLSearchParams(search);
let state = query.get("state");
if (state) {
try {
state = auth.decryptPayload(state);
if (state.email && state.code) {
return state.email;
} else {
throw new Error();
}
} catch {
throw redirect("/login");
}
} else {
return redirect("/login");
}
}, "email");

export const route = {
load: ({location}) => getEmail(location.search)
};

export default function Verify() {
const location = useLocation();
const email = createAsync(() => getEmail(location.search));

return (
<div>email()</div>
);
}
const getEmail = cache(async (search) => {
"use server";
const query = new URLSearchParams(search);
let state = query.get("state");
if (state) {
try {
state = auth.decryptPayload(state);
if (state.email && state.code) {
return state.email;
} else {
throw new Error();
}
} catch {
throw redirect("/login");
}
} else {
return redirect("/login");
}
}, "email");

export const route = {
load: ({location}) => getEmail(location.search)
};

export default function Verify() {
const location = useLocation();
const email = createAsync(() => getEmail(location.search));

return (
<div>email()</div>
);
}
And also, how can I get the redirect to work now?
peerreynders
peerreynders6mo ago
Hypothetically the cache is considered fresh for 10 seconds, so even if the load runs multiple times (which it will) the function wrapped by cache should be safe. One possibility is that something is forcing an invalidation of that cache making it rerun. Is the double call happening only in the redirect case or also in the allow case?
florian
florianOP6mo ago
its running twice for both redirect and allow
florian
florianOP6mo ago
this is when directly loading the page, its running once when server rendering the page and once from the client
No description
peerreynders
peerreynders6mo ago
Fresh install of SolidStart "basic" with the following modification:
// file: src/routes/about.tsx
import { Title } from '@solidjs/meta';
import { cache, createAsync, redirect } from '@solidjs/router';
import type { RouteDefinition, RouteSectionProps } from '@solidjs/router';

const getEmail = cache(async (search: string) => {
'use server';
console.log('getEmail', Date.now());
const query = new URLSearchParams(search);
if (query.get('result') === 'success') return 'success';

throw redirect('/');
}, 'email');

export const route = {
load({ location }) {
console.log('loading email', Date.now());
void getEmail(location.search);
},
} satisfies RouteDefinition;

export default function About(props: RouteSectionProps) {
const result = createAsync(() => getEmail(props.location.search), {
deferStream: true,
});
return (
<main>
<Title>About</Title>
<h1>About</h1>
<p>{result()}</p>
</main>
);
}

/*
* full reload of http://localhost:3000/about?result=success
Server:
loading email 1717898367300
getEmail 1717898367301
loading email 1717898367399
loading email 1717898367400

Client:
loading email 1717898367575

* full reload of http://localhost:3000/about
Server:
loading email 1717898621973
getEmail 1717898621974
loading email 1717898622184
loading email 1717898622186
Client:
*nothing*

* client navigation http://localhost:3000/about?result=success
Server:
getEmail 1717898735476
Client:
loading email 1717898734966
loading email 1717898735262

* client navigation of http://localhost:3000/about
Server:
getEmail 1717898933000
Client:
loading email 1717898932602
loading email 1717898932797

*/
// file: src/routes/about.tsx
import { Title } from '@solidjs/meta';
import { cache, createAsync, redirect } from '@solidjs/router';
import type { RouteDefinition, RouteSectionProps } from '@solidjs/router';

const getEmail = cache(async (search: string) => {
'use server';
console.log('getEmail', Date.now());
const query = new URLSearchParams(search);
if (query.get('result') === 'success') return 'success';

throw redirect('/');
}, 'email');

export const route = {
load({ location }) {
console.log('loading email', Date.now());
void getEmail(location.search);
},
} satisfies RouteDefinition;

export default function About(props: RouteSectionProps) {
const result = createAsync(() => getEmail(props.location.search), {
deferStream: true,
});
return (
<main>
<Title>About</Title>
<h1>About</h1>
<p>{result()}</p>
</main>
);
}

/*
* full reload of http://localhost:3000/about?result=success
Server:
loading email 1717898367300
getEmail 1717898367301
loading email 1717898367399
loading email 1717898367400

Client:
loading email 1717898367575

* full reload of http://localhost:3000/about
Server:
loading email 1717898621973
getEmail 1717898621974
loading email 1717898622184
loading email 1717898622186
Client:
*nothing*

* client navigation http://localhost:3000/about?result=success
Server:
getEmail 1717898735476
Client:
loading email 1717898734966
loading email 1717898735262

* client navigation of http://localhost:3000/about
Server:
getEmail 1717898933000
Client:
loading email 1717898932602
loading email 1717898932797

*/
These results would lead me to conclude that you have something else at work here.
florian
florianOP6mo ago
Tried to find the issue starting with the basic example and adding stuff over slowly and it looks like because I import the server functions like getEmail from a seperate file it doesn't work, specifically because I made the whole file "use server" instead of each funcion specifically Don't know if this is intentionally designed to not work like setting "use server" on each function but it kinda sounds like it should work the same in the docs tbh https://docs.solidjs.com/solid-start/reference/server/use-server at least if it isn't a route where I'm doing that
peerreynders
peerreynders6mo ago
Things still work as expected with this partitioning:
'use server';
// file: src/server/api.ts
import { redirect } from '@solidjs/router';

function getEmail(search: string) {
return new Promise<string>((resolve, reject) => {
setTimeout(() => {
console.log('getEmail', Date.now());
const query = new URLSearchParams(search);
if (query.get('result') === 'success') resolve('success');

reject(redirect('/'));
}, 1000);
});
}

export { getEmail };
'use server';
// file: src/server/api.ts
import { redirect } from '@solidjs/router';

function getEmail(search: string) {
return new Promise<string>((resolve, reject) => {
setTimeout(() => {
console.log('getEmail', Date.now());
const query = new URLSearchParams(search);
if (query.get('result') === 'success') resolve('success');

reject(redirect('/'));
}, 1000);
});
}

export { getEmail };
// file: src/api.ts
import { cache } from '@solidjs/router';
import { getEmail as email } from './server/api';

const getEmail = cache(email, 'email');

export { getEmail };
// file: src/api.ts
import { cache } from '@solidjs/router';
import { getEmail as email } from './server/api';

const getEmail = cache(email, 'email');

export { getEmail };
// file: src/routes/about.tsx
import { Title } from "@solidjs/meta";
import { createAsync } from "@solidjs/router";
import type { RouteDefinition, RouteSectionProps } from "@solidjs/router";

import { getEmail } from '../api';

export const route = {
load({ location }) {
console.log('loading email', Date.now());
void getEmail(location.search);
},
} satisfies RouteDefinition;

export default function About(props: RouteSectionProps) {
const result = createAsync(() => getEmail(props.location.search), { deferStream: true });
return (
<main>
<Title>About</Title>
<h1>About</h1>
<p>{result()}</p>
</main>
);
}
// file: src/routes/about.tsx
import { Title } from "@solidjs/meta";
import { createAsync } from "@solidjs/router";
import type { RouteDefinition, RouteSectionProps } from "@solidjs/router";

import { getEmail } from '../api';

export const route = {
load({ location }) {
console.log('loading email', Date.now());
void getEmail(location.search);
},
} satisfies RouteDefinition;

export default function About(props: RouteSectionProps) {
const result = createAsync(() => getEmail(props.location.search), { deferStream: true });
return (
<main>
<Title>About</Title>
<h1>About</h1>
<p>{result()}</p>
</main>
);
}
florian
florianOP6mo ago
It seems like this solved all my issues I'm having right now. Thanks @Brendonovich and @peerreynders for your help!
Want results from more Discord servers?
Add your server