July 17, 2022

Higher-order components in Svelte

Emulating the React function-as-child pattern.

How do you do higher order components in Svelte?

In React, you will often pass a function as a child when you want to customize the component rendered by an external library. This allows the external library to pass some arguments that your custom component can use. For example, this example from the react-hot-toast docs enables you to render custom toasts:

<Toaster>
  {(toast) => (
    <div
      style={{
        opacity: toast.visible ? 1 : 0,
        background: "white",
        padding: 8,
      }}
    >
      {toast.message}
    </div>
  )}
</Toaster>

The Toaster component will get a children prop of type (toast: Toast) => JSX.Element when this code runs.

This won't directly translate to Svelte, because Svelte doesn't let you define a component inline. It has to be in a .svelte file. But we can still accomplish the same goal in several different ways.

Method 1: A Component prop

The first way to emulate the example from above is the following:

<!-- In Toaster.svelte -->
<script lang="ts">
    import type { SvelteComponent } from 'svelte';
    export let Component: SvelteComponent | null = null;
    const toast = {
        message: 'Hello, world!',
        visible: true
    };
</script>

<svelte:component this={Component} toast={toast} />

You can also pass an element to Component which it can use through a slot:

<svelte:component this={Component} toast={toast}>
  <ToastIcon {toast} slot="icon" />
</svelte:component>

This setup can be used this way:

<!-- In MyComponent.svelte -->
<script lang="ts">
    export let toast: Toast;
</script>

<div class={toast.visible ? `block` : `hidden`}>
    <slot name="icon"></slot>
    {toast.message}
</div>

<!-- Now, pass MyComponent to Toaster -->
<Toaster Component={MyComponent} />

Method 2: Slot your own component

In the previous approach, we used a prop to pass in a component that has a slot. We can also just add a slot to our own component:

<!-- In Toaster.svelte -->
<script lang="ts">
    import type { SvelteComponent } from 'svelte';
    export let Component: SvelteComponent | null = null;
    const toast = {
        message: 'Hello, world!',
        visible: true
    };
</script>

<slot {toast}>
    <!-- Fallback content -->
</slot>

Like the previous approach, you can also provide children to the slotted component. This time, though, you have to use props on the slot:

<slot {toast} {ToastIcon}>
    <!-- Fallback content -->
</slot>

Now, use Toaster as follows:

<!-- In MyComponent.svelte -->
<script lang="ts">
    export let ToastIcon: ToastIcon;
    export let toast: Toast;
</script>

<div class={toast.visible ? `block` : `hidden`}>
    <svelte:component this={ToastIcon} />
    {toast.message}
</div>

<!-- Now, pass MyComponent to Toaster -->
<Toaster let:ToastIcon={ToastIcon}>
    <MyComponent {ToastIcon} />
</Toaster>

You can see these approaches at work in svelte-french-toast, which uses conditional rendering to enable the use of either approach based on the preferences of the library user.

For further reading, check out the Svelte tutorial on slots and <svelte:component>.

Get more like this in your inbox