How to Execute Server Action on Each Route Visit?

I have a home component (code in comments) In summary, what this component does is it calls a server action to check if the user is authenticated. If the user is authenticated, it redirects the user to the dashboard route. Otherwise, it redirects the user to the login route. Now, here's my question. What I want to do is perform this verification every time the home route is visited (even from a client-side redirect). From my logs however, it doesn't look like the server action is being called in subsequent visits to this route (i.e. after the account setup process is complete. The reason I know this is because "Executing checks to determine redirect" is being printed on my console. But, none of the other logs that would be printed if the action completed are. In other words authenticatedUser.result is always falsy. How would I go about doing what I am trying to do? Is there a different approach I should be taking to accomplish what I am trying to do? Thanks. Like I said, component code is in the first comment of this thread.
9 Replies
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
Here's the code:
export default function Home() {
const loadAuthenticatedUser = useAction(getAuthenticatedUserAction);
//const loadAuthenticatedUser = useAction(dummyAction);
const navigate = useNavigate();
loadAuthenticatedUser();
const authenticatedUser = useSubmission(getAuthenticatedUserAction);
const [_, { setUser, clearUser }] = useUserContext();
const [serverError, setServerError] = createSignal("");

createEffect(() => {
console.log("Executing checks to determine redirect.");
if (authenticatedUser.result) {
// If the user is authenticated, we redirect them to the dashboard page.
// otherwise, we show them the main page.
const result = authenticatedUser.result;
console.log("Authenticated User Result present...");
if (result.success) {
console.log("Redirecting to Dashboard...");
setUser(result.data as User);
navigate("/dashboard");
} else {
if (result.data instanceof AppException) {
setServerError(result.message);
console.error(result.message);
} else {
// for now, we ust redirect them to the login page. as soon as we get more traction,
// we will have a proper home page deidicated for marketing.
console.log("Redirecting to login page.");
clearUser();
navigate("/login");
}
}
authenticatedUser.clear();
} else {
console.log(JSON.stringify(authenticatedUser));
}
});

const handleRetry = () => {
setServerError("");
authenticatedUser.retry();
};

return (
<Show
when={!serverError()}
fallback={
<ConfirmationMessage
title="Something went wrong."
description={serverError()}
buttonText="Try Again"
onClick={handleRetry}
image="img/logo.svg"
/>
}
>
<></>
</Show>
);
}
export default function Home() {
const loadAuthenticatedUser = useAction(getAuthenticatedUserAction);
//const loadAuthenticatedUser = useAction(dummyAction);
const navigate = useNavigate();
loadAuthenticatedUser();
const authenticatedUser = useSubmission(getAuthenticatedUserAction);
const [_, { setUser, clearUser }] = useUserContext();
const [serverError, setServerError] = createSignal("");

createEffect(() => {
console.log("Executing checks to determine redirect.");
if (authenticatedUser.result) {
// If the user is authenticated, we redirect them to the dashboard page.
// otherwise, we show them the main page.
const result = authenticatedUser.result;
console.log("Authenticated User Result present...");
if (result.success) {
console.log("Redirecting to Dashboard...");
setUser(result.data as User);
navigate("/dashboard");
} else {
if (result.data instanceof AppException) {
setServerError(result.message);
console.error(result.message);
} else {
// for now, we ust redirect them to the login page. as soon as we get more traction,
// we will have a proper home page deidicated for marketing.
console.log("Redirecting to login page.");
clearUser();
navigate("/login");
}
}
authenticatedUser.clear();
} else {
console.log(JSON.stringify(authenticatedUser));
}
});

const handleRetry = () => {
setServerError("");
authenticatedUser.retry();
};

return (
<Show
when={!serverError()}
fallback={
<ConfirmationMessage
title="Something went wrong."
description={serverError()}
buttonText="Try Again"
onClick={handleRetry}
image="img/logo.svg"
/>
}
>
<></>
</Show>
);
}
For clarification: The action is being executed on the initial page load. However, it isn’t being executed in subsequent visits to the route. Like if you directly enter “my site.com” this route would load and the action would execute. However, if you navigate(“/“) from some other component, the action doesn’t execute. What I am trying to accomplish is to get the action to execute event when the route is loaded via a navigate() call from some other part of my app.
peerreynders
peerreynders7mo ago
Did you review how the example does it? The only "downside" is that the user information is considered fresh for 10 seconds but I believe that could be mitigated by feeding the server function directly to the createAsync. To me using an action for this feels like using POST when I should be using GET.
GitHub
solid-start/examples/with-auth/src/routes/index.tsx at 678d9acbc0bb...
SolidStart, the Solid app framework. Contribute to solidjs/solid-start development by creating an account on GitHub.
GitHub
solid-start/examples/with-auth/src/lib/index.ts at 678d9acbc0bb669f...
SolidStart, the Solid app framework. Contribute to solidjs/solid-start development by creating an account on GitHub.
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
These examples use data loaders. The reason I am using action() is because Data loaders only load on the server once then rely on useResource() to refresh subsequent data. Since I am using a server session, I opted to use Actions instead. In short: No access to useSession() if I were to do this on a createResource() https://docs.solidjs.com/solid-router/concepts/actions Is there a way to run a server function in useResource()? The docs don’t mention it. Either that, or get the data loader to run on every route visit instead of just the initial server-rendered load.
peerreynders
peerreynders7mo ago
"user server" means it runs on the server; doesn't matter if it appears in the route loader.
export const getUser = cache(async () => {
'use server';
try {
const session = await getSession();
const userId = session.data.userId;
if (userId === undefined) throw new Error('User not found');
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) throw new Error('User not found');
return { id: user.id, username: user.username };
} catch {
await logoutSession();
throw redirect('/login');
}
}, 'user');
export const getUser = cache(async () => {
'use server';
try {
const session = await getSession();
const userId = session.data.userId;
if (userId === undefined) throw new Error('User not found');
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) throw new Error('User not found');
return { id: user.id, username: user.username };
} catch {
await logoutSession();
throw redirect('/login');
}
}, 'user');
Where is useResource coming from? (maybe you mean createResource?) And route loaders run on the client side as well. However these days they exist to warm the cache, so if you don't want to use that you should be able to wrap the server function in a createAsync inside the route component.
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
From the Data Loading section of the docs. Under caveats. “The load function is called once per route, which is the first time the user comes to that route. Following that, the fine-grained resources that remain alive synchronize with state/url changes to refetch data when needed. If the data needs a refresh, the refetch function returned in the createResource can be used.”. I need the loader to run every single time the route is visited, not just on the first load. useResource() is client-side only, meaning I cannot access the server session to get auth tokens to check if the user is authenticated (or do anything requiring access to the session for that matter). This is why I opted to use an Action. With action, my thinking was I can call it so it loads every time. At least, that was the idea.
peerreynders
peerreynders7mo ago
which is the first time the user comes to that route.
to my knowledge that should read
whenever the user navigates to that route.
Route loaders are also called when the user just hovers over a link to the route to start warming the cache (which is considered stale after 10secs, 5min for bfcache) even before the navigation starts. However to achieve preloading you have to use cache and createAsync.
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
So, for clarification, suppose this scenario. The user visits the home route where that loader is called (initially). The effect runs, the user isn’t authenticated. So, it redirects the user to the login route. After authenticating, the login route redirects the user back to the home route via navigate(“/“) (meaning, the home route is visited a second time). Will the loader run again on that second visit? My initial solution was to use a data loader. However, it wasn’t running in subsequent visits to the home route. So, I switched to an action.
peerreynders
peerreynders7mo ago
Example
// file: src/routes/quotes.tsx
import { ErrorBoundary, For } from 'solid-js';
import { Title } from '@solidjs/meta';
import { createAsync, type RouteDefinition } from '@solidjs/router';
import { quotes as getQuotes } from '../api';

// https://docs.solidjs.com/solid-router/reference/load-functions/load
export const route = {
load() {
console.log('loading quotes');
void getQuotes();
},
} satisfies RouteDefinition;

function Quotes() {
// https://docs.solidjs.com/solid-router/reference/data-apis/create-async
const quotes = createAsync(() => getQuotes());
return (
<main>
<Title>Quotes</Title>
<ErrorBoundary
fallback={(err: Error) => <p>Problem loading quotes: {err.message}</p>}
>
<h1>Quotes</h1>
<ul class="c-quote-list">
<For each={quotes()}>
{(item) => (
<li>
<span class="c-quote-item__author">{item.author}</span>
{': '}
<span class="c-quote-item__quote">{item.quote}</span>
</li>
)}
</For>
</ul>
</ErrorBoundary>
</main>
);
}

export { Quotes };
// file: src/routes/quotes.tsx
import { ErrorBoundary, For } from 'solid-js';
import { Title } from '@solidjs/meta';
import { createAsync, type RouteDefinition } from '@solidjs/router';
import { quotes as getQuotes } from '../api';

// https://docs.solidjs.com/solid-router/reference/load-functions/load
export const route = {
load() {
console.log('loading quotes');
void getQuotes();
},
} satisfies RouteDefinition;

function Quotes() {
// https://docs.solidjs.com/solid-router/reference/data-apis/create-async
const quotes = createAsync(() => getQuotes());
return (
<main>
<Title>Quotes</Title>
<ErrorBoundary
fallback={(err: Error) => <p>Problem loading quotes: {err.message}</p>}
>
<h1>Quotes</h1>
<ul class="c-quote-list">
<For each={quotes()}>
{(item) => (
<li>
<span class="c-quote-item__author">{item.author}</span>
{': '}
<span class="c-quote-item__quote">{item.quote}</span>
</li>
)}
</For>
</ul>
</ErrorBoundary>
</main>
);
}

export { Quotes };
If you open the browser's developer console you will see loading quotes whenever you even threaten to navigate to /quotes. So there must have been something else going on with your setup.
peerreynders
StackBlitz
Quick fetch example - StackBlitz
A Solid TypeScript project based on @solidjs/meta, @solidjs/router, solid-js, typescript, vite and vite-plugin-solid
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
Hmm. Alright. Let me see what else is going on.
Want results from more Discord servers?
Add your server