Search code examples
javascriptsveltesveltekit

How do I create an inline Higher Order Component in SvelteKit 2.5?


I'm asking in general, but I'll give you an example use-case. On a particular page I'm building, I'm using the Helper component from Flowbite-Svelte. I keep finding myself using <Helper color="red">...</Helper> to make the color red. It'd be nice to build a higher order component and just use that, e.g. <ErrorHelper>...</ErrorHelper>, where ErrorHelper automatically passes the color prop as red to Helper.

I've been looking on the web for an hour and can't find out how to make a higher order component. I tried myself and came up with this:

import { Helper } from 'flowbite-svelte';

class ErrorHelper extends Helper {
  constructor(vals) {
  const newVals = {...vals, props: {...vals.props, color: 'red'}}
    super(newVals);
  }
}

This works when I render the component using Storybook for Svelte, which I think uses Svelte WITHOUT using SvelteKit. But when I use the same code in my SvelteKit website, like this:

// src/routes/onboard/3/+page.svelte
<script lang="ts">
    import OnboardStep3 from '$lib/components/OnboardStep3.svelte';
</script>

<OnboardStep3 />

I get this error:

TypeError: Class extends value #<Object> is not a constructor or null
    at /src/lib/components/OnboardStep3.svelte:17:28
    at Object.$$render (/node_modules/svelte/src/runtime/internal/ssr.js:156:16)
    at eval (/src/routes/onboard/3/+page.svelte:15:100)
    at Object.$$render (/node_modules/svelte/src/runtime/internal/ssr.js:156:16)
    at Object.default (/.svelte-kit/generated/root.svelte:48:43)
    at /node_modules/@sveltejs/kit/src/runtime/components/layout.svelte:5:41
    at Object.$$render (/node_modules/svelte/src/runtime/internal/ssr.js:156:16)
    at Object.default (/.svelte-kit/generated/root.svelte:47:42)
    at eval (/src/routes/+layout.svelte:25:163)
    at Object.$$render (/node_modules/svelte/src/runtime/internal/ssr.js:156:16)

Solution

  • Unless you disable SSR in SvelteKit, you will have to also create a valid SSR-component.

    Unfortunately Svelte uses some internals rather than the public SSR API. I.e. you cannot just provide an object with a render function but need to provide and use $$render instead. (Both the client-side and the server-side APIs are significantly different in Svelte 5, so migration will be necessary either way.)

    Here is an example:

    import { browser } from '$app/environment';
    import { Helper } from 'flowbite-svelte';
    
    export const ErrorHelper = extend(Helper, { color: 'red' });
    
    function extend(component, defaults) {
        const extendProps = props => ({ ...defaults, ...props });
    
        return browser
            ? class extends component {
                constructor(options) {
                    const { props, ...rest } = options;
                    super({
                        ...rest,
                        props: extendProps(props),
                    });
                }
            }
            : {
                $$render(result, props, bindings, slots, context) {
                    return component.$$render(
                        result,
                        extendProps(props),
                        bindings,
                        slots,
                        context
                    );
                }
            }
    }