solid-router causes error: "cleanups created outside a `createRoot` or `render` will never be run"

Hi I am relatively new to solid so I could be making a simple mistake. I recently added solid router to my solid.js project and I am using the config based approach. Since I added the router and using it I get multiple errors like this: - cleanups created outside a createRoot or render will never be run - computations created outside a createRoot or render will never be disposed Since the router is actually rendered inside the render() method of solid, I am not sure what I am doing wrong, any help how to fix the warnings would be appreciated. Below you can see my code (I also uploaded the full files because pasting entire code is higher than the discord message limit. index.tsx
const wrapper = document.getElementById("root");

if (!wrapper) {
throw new Error("Wrapper div not found");
}

const routes = [
{
path: "/",
component: lazy(() => import("./App.tsx")),
},
{
path: "/players",
component: lazy(() => import("./components/players/Players.tsx")),
},
];
render(() => <Router root={MainLayout}>{routes}</Router>, wrapper);
const wrapper = document.getElementById("root");

if (!wrapper) {
throw new Error("Wrapper div not found");
}

const routes = [
{
path: "/",
component: lazy(() => import("./App.tsx")),
},
{
path: "/players",
component: lazy(() => import("./components/players/Players.tsx")),
},
];
render(() => <Router root={MainLayout}>{routes}</Router>, wrapper);
App.tsx
export default function App() {
const [session, setSession] = createSignal<Session | null>(null);

// On component mount, get the current session and subscribe to auth state changes.
onMount(async () => {
const {
data: { session: currentSession },
} = await supabase.auth.getSession();
setSession(currentSession);

const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});

// Cleanup the subscription when the component unmounts.
// THIS ACTUALLY THROWS THE ERROR: cleanups created outside a `createRoot` or `render` will never be run
onCleanup(() => {
subscription.unsubscribe();
});
});

return (
<>
.....
</>
);
}
export default function App() {
const [session, setSession] = createSignal<Session | null>(null);

// On component mount, get the current session and subscribe to auth state changes.
onMount(async () => {
const {
data: { session: currentSession },
} = await supabase.auth.getSession();
setSession(currentSession);

const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});

// Cleanup the subscription when the component unmounts.
// THIS ACTUALLY THROWS THE ERROR: cleanups created outside a `createRoot` or `render` will never be run
onCleanup(() => {
subscription.unsubscribe();
});
});

return (
<>
.....
</>
);
}
3 Replies
peerreynders
peerreynders2d ago
Mounting is a synchronous operation (i.e. the component DOM and it's reactive context has now been connected to the live DOM). Because you are using an async function by the time onCleanup is reached the original runtime context that created the promise is already gone which includes the owner of the reactive context against which the clean up would be registered. Also I'm getting the feeling that type of code should really exist in the Router's root layout, perhaps in a context or handled via action/query. Is there a particular example that you are trying to port / emulate? (like this) Disclaimer: I know nothing of Supabase.
peerreynders
peerreyndersthis hour
You are using
import { Auth } from "@supabase/auth-ui-solid";
import { Auth } from "@supabase/auth-ui-solid";
Note:
Auth.UserContextProvider = UserContextProvider
Auth.useUser = useUser
Auth.UserContextProvider = UserContextProvider
Auth.useUser = useUser
It already gives you a context which you can use which does most of the work for you. (Personally I'd replace it with my own context as this one doesn't expose the supabaseClient via the context value which is a pain because Auth requires a supabaseClient prop). Now put Auth.UserContextProvider at the root of MainLayout. That way any nested component can use const auth = Auth.useUser() to get access to the auth.user and auth.session signals. Now given how their Auth component works the following isn't ideal from the solid-router perspective (where you would have a login action that throws a redirect("/") after a successful login and throw redirect("/login") in a logout action). Layouts get RouteSectionProps and you want the props.location.
GitHub
auth-ui/packages/solid/src/components/Auth/UserContext.tsx at 5ccb3...
Pre-built Auth UI for React. Contribute to supabase-community/auth-ui development by creating an account on GitHub.
GitHub
auth-ui/packages/solid/src/components/Auth/Auth.tsx at 5ccb32c38624...
Pre-built Auth UI for React. Contribute to supabase-community/auth-ui development by creating an account on GitHub.
GitHub
solid-router/src/types.ts at 3c214ce2ceb9b7d9d39d143229a8c6145e83e6...
A universal router for Solid inspired by Ember and React Router - solidjs/solid-router
peerreynders
peerreynders23h ago
In MainLayout:
const navigate = useNavigate();
const auth = Auth.useUser();
createEffect(() => {
if (props.location.pathname !== '/login' && !auth.user()) navigate('/login');
});
const navigate = useNavigate();
const auth = Auth.useUser();
createEffect(() => {
if (props.location.pathname !== '/login' && !auth.user()) navigate('/login');
});
useNavigate() (Note: You may need to split MainLayout into two parts where the nested component makes use of the Auth.useUser() hook while the container sets the provider.) Create a '/login' route for the Auth component. And there:
const auth = Auth.useUser();
createEffect(() => {
if (auth.user()) navigate('/');
});
const auth = Auth.useUser();
createEffect(() => {
if (auth.user()) navigate('/');
});
Now you could get fancy and support a props.params.redirectTo (params being included in the RouteSectionProps) encoded query parameter in your login route (set by MainLayout on navigate).
// file: client-context.ts
import { createContext, useContext } from 'solid-js';

import type { SupabaseClient } from '@supabase/supabase-js';

const ClientContext = createContext<SupabaseClient | undefined>();

export interface Props {
client: SupabaseClient;
}

function ClientProvider(props: Props) {
return (
<ClientContext.Provider value={props.client}>
props.children
</ClientContext.Provider>
);
}

function useClient() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error(`useClient must be used within a ClientProvider.`);
}
return context;
}

export { ClientProvider, useClient };
// file: client-context.ts
import { createContext, useContext } from 'solid-js';

import type { SupabaseClient } from '@supabase/supabase-js';

const ClientContext = createContext<SupabaseClient | undefined>();

export interface Props {
client: SupabaseClient;
}

function ClientProvider(props: Props) {
return (
<ClientContext.Provider value={props.client}>
props.children
</ClientContext.Provider>
);
}

function useClient() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error(`useClient must be used within a ClientProvider.`);
}
return context;
}

export { ClientProvider, useClient };
If you place
<ClientProvider client={client}>
<Auth.UserContextProvider supabaseClient={client}>
/* … */
</Auth.UserContextProvider>
</ClientProvider>
<ClientProvider client={client}>
<Auth.UserContextProvider supabaseClient={client}>
/* … */
</Auth.UserContextProvider>
</ClientProvider>
at the top of the Router root layout you can use const client = useClient() in the /login route for the Auth component.

Did you find this page helpful?