S
SolidJS2mo ago
Joshua

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 (
<>
.....
</>
);
}
13 Replies
peerreynders
peerreynders2mo 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
peerreynders2mo ago
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
peerreynders2mo 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.
Joshua
JoshuaOP2mo ago
@peerreynders First of all I really want toto thank you for the detailed answer and explanations that is very helpful! Exactly I was trying to do the basic example for react supabase auth with solid. I know react quite well and there I could write a Celan up function in an effect. But I now understand that in solid I cannot do that because of reactive context gets breaken when using async. Is there a page in the docs or a good yt video or something that explains the reactive context? I understand signals and reactivity but the reactive context specific to solid I think I need to read up on. I fixed it by ensuring the cleanup is created before reactive context is gone because of await and now the error is gone see my new MainLayout.tsx But I still have below error for a different part of the code, and before I added the router I did not have this issue: createLocalStorageStore.tsx:21 computations created outside a createRoot or render will never be disposed I have this custom hook createLlocalStorageStore, and before I added the router it worked fine. Now it still works but I get this, why? I know I probably also could somehow solve this with a context, but since it is a single hook and I know it will always only be used in one component, I read that you can just create a store outside a component in a file ts without having to use a context etc that this is fine in solidjs. But why does the warning now show up after adding the router or how would I fix the warning? The relevant files () are attached. Also ofc you are right that normally such code would go into a context but since I will put the login as the main page and use a redirect and not need to have access in my whole app to the user I decided to to it simple as POC directly without context, since with supabase you can get the user anyway anywhere in the app via supabase.auth.getSession() but ofc course if it grows etc then a context would make sense so thanks for the guidance.
Joshua
JoshuaOP2mo ago
peerreynders
peerreynders2mo ago
createLocalStorageStore.tsx:21 computations created outside a createRoot or render will never be disposed
The problem is here
export const [gameState, setGameState] = createLocalStorageStore<GameState>(
'gameState',
{
timer: 0,
gameRunning: false,
goalHistory: [],
}
);
export const [gameState, setGameState] = createLocalStorageStore<GameState>(
'gameState',
{
timer: 0,
gameRunning: false,
goalHistory: [],
}
);
This code is run when the module is imported which means it's not running under a reactive context and that's a problem for the createEffect inside the hook as it cannot register it's cleanup anywhere. createLocalStorageStore is fine inside a component setup (in a layout to be passed via props or provider to be passed via context) because component setup functions run within the reactive context created by render at the root of the reactive graph. In simple terms internally createEffect runs getOwner and is unhappy when it doesn't get one.
peerreynders
peerreynders2mo ago
“SolidJS is a state library that happens to render”. Compare that to React which is only interested in state inasfar as it relates to rendering; that's an entirely different perspective. So while in Solid state doesn't have to be monolithic, to maintain its reactivity its supporting reactive graph has to grow from the root established by the render function. Solid's compiler is largely concerned with transforming JSX. Svelte uses their compiler to manage reactivity; that's not the case with Solid; its reactivity is managed at runtime, starting from render and growing from there. That's why in Solid props are reactive. The container component passes a reactive dependency to a nested component; that way when that dependency changes that change can propagate into the reactive subgraph that the nested component created at setup time.
Ryan Carniato
YouTube
Components Are Pure Overhead
With many starting to look at incorporating Reactivity into existing Frameworks I wanted to take a moment to look at how Frameworks evolved into using Components, the evolution of change and state management, to best understand the impact of where things are going. [0:00] Preamble [12:02] Change Management [40:51] MVC to Components [1:12:34] Th...
peerreynders
peerreynders2mo ago
since with supabase you can get the user anyway anywhere in the app via supabase.auth.getSession()
Stop right there. Retrieve a session That's an async function. Now that you are using the @solidjs/router async values should be wrapped in createAsync. createAsync acts as a “port” for updates of a (settling) async value into the reactive realm (which is really synchronous, not in a fake synchronous await kind of way). Solid 2.x will integrate async in a slightly different way but for the time being async has no business inside the reactive graph; async uses createAsync as an adapter to interact with the reactive graph. what color is your function In a reactive world you don't tend to (imperatively) get something when you need it but instead you try to have it ready by the time you need it; or more to the point: things change either because of your actions or external events and that change propagates to adjust all the things the depend on it. Furthermore supabase ideally wants you to Listen to auth events and that is exactly how their context works. getSession is only used to get the initial value, past that events update the session and user signals.
JavaScript: Retrieve a session | Supabase Docs
Supabase API reference for JavaScript: Retrieve a session
JavaScript: Listen to auth events | Supabase Docs
Supabase API reference for JavaScript: Listen to auth events
peerreynders
peerreynders2mo ago
As Mark Erikson already stated years ago Context is a form of Dependency Injection And Solid's context is even simpler, it's not reactive. The context value simply holds things that are already reactive. For example this store is completely unnecessary (the fact that setValue isn't used anywhere is the give away). A simple object literal would be enough here as the session and user accessor's are already reactive. So I would argue that context is the natural way of injecting dependencies (like user, session, gameState) that need to be accessible at arbitrary points within the reactive graph. Now, if it is just a container passing a dependency to a nested component then props is good enough.
Mark's Dev Blog
Blogged Answers: Why React Context is Not a "State Management" Tool...
Definitive answers and clarification on the purpose and use cases for Context and Redux
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.
peerreynders
peerreynders2mo ago
The only other way to sanely integrate async server state is query while using action to update that server state. One query can be shared by multiple createAsync which will all be updated when that one query is updated. https://discord.com/channels/722131463138705510/1330302780250001449/1330366296956735582 But the supabase client API seems to favour events and for that context + signals works better.
peerreynders
peerreynders2mo ago
Is there a page in the docs or a good yt video or something that explains the reactive context?
I find that Building a Reactive Library from Scratch is at the core of understanding Solid. In particular
const context = [];

function subscribe(running, subscriptions) {
subscriptions.add(running);
running.dependencies.add(subscriptions);
}

export function createSignal(value) {
const subscriptions = new Set();

const read = () => {
const running = context[context.length - 1];
if (running) subscribe(running, subscriptions);
return value;
};

const write = (nextValue) => {
value = nextValue;

for (const sub of [...subscriptions]) {
sub.execute();
}
};
return [read, write];
}
const context = [];

function subscribe(running, subscriptions) {
subscriptions.add(running);
running.dependencies.add(subscriptions);
}

export function createSignal(value) {
const subscriptions = new Set();

const read = () => {
const running = context[context.length - 1];
if (running) subscribe(running, subscriptions);
return value;
};

const write = (nextValue) => {
value = nextValue;

for (const sub of [...subscriptions]) {
sub.execute();
}
};
return [read, write];
}
and later
export function createEffect(fn) {
const execute = () => {
cleanup(running);
context.push(running);
try {
fn();
} finally {
context.pop();
}
};

const running = {
execute,
dependencies: new Set()
};

execute();
}
export function createEffect(fn) {
const execute = () => {
cleanup(running);
context.push(running);
try {
fn();
} finally {
context.pop();
}
};

const running = {
execute,
dependencies: new Set()
};

execute();
}
Note the context.push(running); before the effect function is run; without that context variable, subscriptions necessary for change propagation cannot be managed. This is an oversimplified demonstration of why an owner is necessary. In reality of course a reactive root is responsible for disposing of all the reactive resources that were created under it.
DEV Community
Building a Reactive Library from Scratch
In the previous article A Hands-on Introduction to Fine-Grained Reactivity I explain the concepts...
peerreynders
StackBlitz
Root And Run - StackBlitz
How to use Solid's createRoot
peerreynders
peerreynders2mo ago
What the article doesn't highlight is that createEffect should only be used when you are leaving the reactive graph. So using createEffect to subscribe to one reactive dependency to set another signal/store is usually a mistake (there are rare exceptions that Solid 2.x will mostly eliminate) and usually pave the road to infinite reactive loops. For the reactive graph to operate efficiently consumers need to be directly subscribed to their dependencies, createEffect breaks the direct connection within the reactive graph between the dependency and the consumer; given that effects don't run until the values in the reactive graph are stable, effects that set reactive sources start updating the reactive graph all over again. Ideally effects only adjust the outside world to the current state of the reactive graph.
GitHub
strello/src/components/Board.tsx at 9c9ae973d96cc045914e696757a1b5f...
Contribute to solidjs-community/strello development by creating an account on GitHub.
peerreynders
peerreynders2mo ago
For cases like optimistic updates you can use createWritableMemo. The concept here is that the dependencies inside the effect function do have the ultimate authority on the value exposed by the memo. But here you can use the setter to temporarily override the memo value which will remain until the dependencies update again. So for an optimistic update you provide the memo's value “optimistically” so that by the time the real dependencies catch up, the update work has already been done. Afaik that's how signals in 2.x will work anyway.
Solid Primitives
A library of high-quality primitives that extend SolidJS reactivity

Did you find this page helpful?