sveltejs / kit

web development, streamlined
https://svelte.dev/docs/kit
MIT License
18.76k stars 1.96k forks source link

Share/Inject functions and object instances in universal loaders without globals/singletons #12455

Open Rican7 opened 4 months ago

Rican7 commented 4 months ago

Describe the problem

When working with universal loaders (+page.js and +layout.js), there's (seemingly) no way to pass around functions or object instances without resorting to globals or singletons.

On the server-side, you can initialize objects in hooks.server.js and then pass them to server loaders via event.locals (which has it's own oddities of naming/documentation), but there doesn't seem to be an analogous way to pass these kinds of things in a client or universal context.

In components I can use stores/context, or just drill props, but there just doesn't seem to be a way to easily share complex or expensive to initialize objects in client/universal loaders without using globals/singletons.... which comes with all of the negatives of using globals.

Describe the proposed solution

I'd like to, generally, have a way to define a init function (or constructor?) or some way to define dependencies in loaders that isn't just importing globals.

If that's not something that is to be considered, then I'd like to at least see a way for client/universal loaders to share complex data that's similar to the server loaders capabilities.

Alternatives considered

Just using globals/singletons... but that complicates testing and comes with all of the drawbacks of globals/singletons.

Importance

would make my life easier

Additional Information

This may be similar to #6714 or #7107... but from a different functional perspective.

david-plugge commented 4 months ago

event.parent is the intended way of doing this, but it is a little annoying as it results in a waterfall. I proposed event.locals for shared load functions and a shared handleLoad hook some time ago. Would be awesome to get this feature for spas!

Rican7 commented 4 months ago

event.parent is the intended way of doing this, but it is a little annoying as it results in a waterfall.

Yea, that causes waterfalls and it has to be initiated in a top-level layout or something, which is inconsistent with the way it works with a server (using hooks).

I proposed event.locals for shared load functions and a shared handleLoad hook some time ago. Would be awesome to get this feature for spas!

Yea, honestly that would make things more consistent with how it works for the server. Which, again as I said still isn't the clearest mechanism, but at least that would be consistent. The lack of common "entry-point" or "main" for an SK app makes this all a bit confusing and hard to discover.

Crisfole commented 2 months ago

Moving conversation from here:

https://discord.com/channels/457912077277855764/1287826942376149088

This particular request is basically the same as my question in the above discord chat.

I have a Supabase client that I initialize in my root layout:

src/routes/+layout.server.ts

export const load = async ({ locals: { session, user } }) => {
    return {
        session,
    };
};

src/routes/+layout.ts

import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from "$env/static/public";
import { combineChunks, createBrowserClient, isBrowser, parse } from "@supabase/ssr";

export const load = async ({ fetch, data, depends }) => {
    depends("supabase:auth");

    const supabase = createBrowserClient(
        PUBLIC_SUPABASE_URL,
        PUBLIC_SUPABASE_ANON_KEY,
        {
            global: { fetch },
            cookies: {
                get(key) {
                    if (!isBrowser()) {
                        return JSON.stringify(data.unvalidated_session);
                    }
                    const cookie = combineChunks(key, (name) => {
                        const cookies = parse(document.cookie);
                        return cookies[name];
                    });
                    return cookie;
                },
            },
        },
    );

    return { supabase };
};

I have nested layouts. In order to re use this client anywhere other than a component (which is not the 'blessed' Svelte Kit way to load data) I have to await parent() which introduces wholly unnecessary waterfalls. The other alternatives seem to be re-creating the client in every downstream layout or abandoning universal loaders.

I'd love to see the client equivalent of hooks. Or (maybe better?) a way to introduce specific parent dependencies:

// Where the parent is the value from `event.route.id`
const { supabase } = await parent("/");

This would allow introducing waterfalls that are needed without forcing it to be strictly sequential.

david-plugge commented 1 month ago

I worked out a solution for universal load functions: https://gist.github.com/david-plugge/33cd821a4c829059f67ce18468b8dfca. The same would work for server load functions aswell. Its definately a hack but still better than a waterfall. This should be built into sveltekit.