Exception thrown when invoking useNavigate within Router root layout

I am having a real hard time working through this problem. I feel like I've tried everything but nothing has been working and there's probably something I'm not seeing clearly anymore. The short of it is, when I attempt to invoke navigate("/login") from my root layout component, I am greeted with the following exception:
TypeError: Cannot read properties of undefined (reading 'path')
TypeError: Cannot read properties of undefined (reading 'path')
Here is the entirety of my App.tsx file:
import "./app.css";

import { attachDevtoolsOverlay } from "@solid-devtools/overlay";

import { createAsync, Route, Router, type RouteSectionProps, useNavigate } from "@solidjs/router";
import CircleUser from "lucide-solid/icons/circle-user";
import Clock from "lucide-solid/icons/clock-4";
import LogOut from "lucide-solid/icons/log-out";
import { createEffect, ErrorBoundary, lazy, type ParentProps, Show, Suspense } from "solid-js";

import { isMedium } from "./globalSignals";
import { authClient } from "./lib/auth-client";
import { getAuthState } from "./lib/queries";
import { cn, isNil } from "./lib/utils";
import { Navbar, NavbarLink } from "./Navbar";

const Login = lazy(() => import("./pages/Login"));
const Home = lazy(() => import("./pages/Dashboard"));

attachDevtoolsOverlay();

const MainContent = (props: ParentProps) => {
return <main class="flex flex-col h-full w-full flex-grow p-4">{props.children}</main>;
};

const RootLayout = (props: RouteSectionProps<unknown>) => {
const navigate = useNavigate();
const auth = createAsync(() => getAuthState());

return (
<div id="app-screen" class="h-screen w-screen">
<Show when={!isNil(auth())} fallback={<MainContent {...props} />}>
<div
id="root-layout"
class={cn([
"h-full",
isMedium() ? "grid grid-rows-[1fr] grid-cols-[auto_1fr]" : "flex flex-col",
])}
>
<Navbar>
<NavbarLink href="/dashboard" icon={<CircleUser />}>
Dashboard
</NavbarLink>
<NavbarLink href="/timesheets" icon={<Clock />}>
Timesheets
</NavbarLink>
<hr class="my-3" />
<NavbarLink
href="#"
icon={<LogOut height={30} />}
onClick={() => {
authClient.signOut({
fetchOptions: {
onSuccess: () => {
navigate("/login");
},
},
});
}}
>
Sign Out
</NavbarLink>
</Navbar>
<MainContent {...props} />
</div>
</Show>
</div>
);
};

const ActiveUserSession = (props: ParentProps) => {
const navigate = useNavigate();
const auth = createAsync(() => getAuthState());

createEffect(() => {
if (!auth()) {
navigate("/login");
}
});

return (
<Show when={!isNil(auth())}>
<Suspense>{props.children}</Suspense>
</Show>
);
};

export const App = () => {
return (
<ErrorBoundary fallback={<div>Oops! Something went wrong!</div>}>
<Router
root={(props) => (
<Suspense>
<RootLayout {...props} />
</Suspense>
)}
>
<Route path="/login" component={Login} />
<Route
path="/"
component={(props) => (
<Suspense>
<ActiveUserSession>{props.children}</ActiveUserSession>
</Suspense>
)}
>
<Route path="/dashboard" component={Home} />
</Route>
<Route path="*" component={() => <div>Not Found</div>} />
</Router>
</ErrorBoundary>
);
};
import "./app.css";

import { attachDevtoolsOverlay } from "@solid-devtools/overlay";

import { createAsync, Route, Router, type RouteSectionProps, useNavigate } from "@solidjs/router";
import CircleUser from "lucide-solid/icons/circle-user";
import Clock from "lucide-solid/icons/clock-4";
import LogOut from "lucide-solid/icons/log-out";
import { createEffect, ErrorBoundary, lazy, type ParentProps, Show, Suspense } from "solid-js";

import { isMedium } from "./globalSignals";
import { authClient } from "./lib/auth-client";
import { getAuthState } from "./lib/queries";
import { cn, isNil } from "./lib/utils";
import { Navbar, NavbarLink } from "./Navbar";

const Login = lazy(() => import("./pages/Login"));
const Home = lazy(() => import("./pages/Dashboard"));

attachDevtoolsOverlay();

const MainContent = (props: ParentProps) => {
return <main class="flex flex-col h-full w-full flex-grow p-4">{props.children}</main>;
};

const RootLayout = (props: RouteSectionProps<unknown>) => {
const navigate = useNavigate();
const auth = createAsync(() => getAuthState());

return (
<div id="app-screen" class="h-screen w-screen">
<Show when={!isNil(auth())} fallback={<MainContent {...props} />}>
<div
id="root-layout"
class={cn([
"h-full",
isMedium() ? "grid grid-rows-[1fr] grid-cols-[auto_1fr]" : "flex flex-col",
])}
>
<Navbar>
<NavbarLink href="/dashboard" icon={<CircleUser />}>
Dashboard
</NavbarLink>
<NavbarLink href="/timesheets" icon={<Clock />}>
Timesheets
</NavbarLink>
<hr class="my-3" />
<NavbarLink
href="#"
icon={<LogOut height={30} />}
onClick={() => {
authClient.signOut({
fetchOptions: {
onSuccess: () => {
navigate("/login");
},
},
});
}}
>
Sign Out
</NavbarLink>
</Navbar>
<MainContent {...props} />
</div>
</Show>
</div>
);
};

const ActiveUserSession = (props: ParentProps) => {
const navigate = useNavigate();
const auth = createAsync(() => getAuthState());

createEffect(() => {
if (!auth()) {
navigate("/login");
}
});

return (
<Show when={!isNil(auth())}>
<Suspense>{props.children}</Suspense>
</Show>
);
};

export const App = () => {
return (
<ErrorBoundary fallback={<div>Oops! Something went wrong!</div>}>
<Router
root={(props) => (
<Suspense>
<RootLayout {...props} />
</Suspense>
)}
>
<Route path="/login" component={Login} />
<Route
path="/"
component={(props) => (
<Suspense>
<ActiveUserSession>{props.children}</ActiveUserSession>
</Suspense>
)}
>
<Route path="/dashboard" component={Home} />
</Route>
<Route path="*" component={() => <div>Not Found</div>} />
</Router>
</ErrorBoundary>
);
};
The area to focus on is the onClick for the Sign Out NavbarLink. In the callback, I am invoking the Better Auth client to sign out and on success, I navigate to /login. It's that invocation that causes the exception. I'm not exactly sure the proper way to debug this and could use all the help I can get
1 Reply
MaveriX89
MaveriX89OP7h ago
Just adding more additional context here. The NavbarLink components are just <A> elements from @solidjs/router wrapped under <li> tags. And for reference, here is my Login.tsx:
import { action, createAsync, redirect, useNavigate, useSubmission } from "@solidjs/router";
import { createEffect, Show } from "solid-js";

import { Button } from "~/components/ui/button";
import { TextField, TextFieldInput, TextFieldLabel } from "~/components/ui/text-field";

import Logo from "../assets/logo_login.svg";
import { authClient } from "../lib/auth-client";
import { getAuthState } from "../lib/queries";
import { convertToRecord, isNil } from "../lib/utils";

type SignInForm = {
readonly username: string;
readonly password: string;
};

const authenticate = action(async (data: FormData) => {
const response = await authClient.signIn.username(convertToRecord<SignInForm>(data));

if (response.error) throw new Error(response.error.message);

throw redirect("/dashboard");
});

export default function SignIn() {
const navigate = useNavigate();
const auth = createAsync(() => getAuthState());
const authenticating = useSubmission(authenticate);

createEffect(() => {
if (!isNil(auth())) {
navigate("/dashboard");
}
});

return (
<div class="h-full flex flex-col items-center justify-center gap-6">
<img src={Logo} width="200" />
<form action={authenticate} method="post" class="flex flex-col gap-6 w-[300px]">
<TextField>
<TextFieldLabel class="mb-2">Username</TextFieldLabel>
<TextFieldInput name="username" autocomplete="username" />
</TextField>
<TextField>
<TextFieldLabel class="mb-2">Password</TextFieldLabel>
<TextFieldInput name="password" type="password" autocomplete="current-password" />
</TextField>
<Button class="button" type="submit" disabled={authenticating.pending}>
{authenticating.pending ? "Authenticating..." : "Sign In"}
</Button>
</form>
<Show when={authenticating.error}>
<p style={{ color: "red" }} role="alert" id="error-message">
{authenticating.error?.message}
</p>
</Show>
</div>
);
}
import { action, createAsync, redirect, useNavigate, useSubmission } from "@solidjs/router";
import { createEffect, Show } from "solid-js";

import { Button } from "~/components/ui/button";
import { TextField, TextFieldInput, TextFieldLabel } from "~/components/ui/text-field";

import Logo from "../assets/logo_login.svg";
import { authClient } from "../lib/auth-client";
import { getAuthState } from "../lib/queries";
import { convertToRecord, isNil } from "../lib/utils";

type SignInForm = {
readonly username: string;
readonly password: string;
};

const authenticate = action(async (data: FormData) => {
const response = await authClient.signIn.username(convertToRecord<SignInForm>(data));

if (response.error) throw new Error(response.error.message);

throw redirect("/dashboard");
});

export default function SignIn() {
const navigate = useNavigate();
const auth = createAsync(() => getAuthState());
const authenticating = useSubmission(authenticate);

createEffect(() => {
if (!isNil(auth())) {
navigate("/dashboard");
}
});

return (
<div class="h-full flex flex-col items-center justify-center gap-6">
<img src={Logo} width="200" />
<form action={authenticate} method="post" class="flex flex-col gap-6 w-[300px]">
<TextField>
<TextFieldLabel class="mb-2">Username</TextFieldLabel>
<TextFieldInput name="username" autocomplete="username" />
</TextField>
<TextField>
<TextFieldLabel class="mb-2">Password</TextFieldLabel>
<TextFieldInput name="password" type="password" autocomplete="current-password" />
</TextField>
<Button class="button" type="submit" disabled={authenticating.pending}>
{authenticating.pending ? "Authenticating..." : "Sign In"}
</Button>
</form>
<Show when={authenticating.error}>
<p style={{ color: "red" }} role="alert" id="error-message">
{authenticating.error?.message}
</p>
</Show>
</div>
);
}
I make heavy use of the getAuthState query which you can see here:
import { query } from "@solidjs/router";

import { authClient } from "./auth-client";
import { isNil } from "./utils";

export const getAuthState = query(async () => {
const session = await authClient.getSession();

return isNil(session) || isNil(session.data)
? null
: {
user: session.data.user,
session: session.data.session,
error: session.error,
};
}, "authState");
import { query } from "@solidjs/router";

import { authClient } from "./auth-client";
import { isNil } from "./utils";

export const getAuthState = query(async () => {
const session = await authClient.getSession();

return isNil(session) || isNil(session.data)
? null
: {
user: session.data.user,
session: session.data.session,
error: session.error,
};
}, "authState");
From the looks of it, I do see the url change in my app showing that it went to /login -- however, everytime it does, the <ErrorBoundary> fallback component kicks in and displays instead of the Login page I defined above due to that thrown exception.

Did you find this page helpful?