articles

Renderless Components in Svelte

Published on October 13, 2020

For those who have missed my passage at Svelte Society Day France 2020 and not seen my glorious talk on Renderless Component (or those who, you know … don’t speak French ?) this article should cover the basics of what was said in the talk, but in a more concise structured manner considering French is not my mother tongue (for those interested: it’s Dutch).

What are Renderless Components ?

At the very base “Renderless Components” are components that contain no markup or styling of their own, they have no rendering of their own, hence the name ‘renderless’. What do they have is at least a slot element to be able to embed other components inside.

<slot></slot>

If that looks ridicilous to you, it is because it is. It would be completely useless. But a Svelte component is more than just the markup or the styling, there is also JavaScript involved, so a renderless component will look more like this:

<script>
    // something goes here
</script>

<slot></slot>

The part that goes in the script tag would be common behaviour to a specific problem that is not necessarily style specific. If that sounded difficiult, no worries in the examples it will become clear. For now remember this:

Renderless Components offer an abstraction of common behaviour and logic in a flexible, non-stylistic way.

Example 1: An Accordion

In the code below we have a simple accordion implementation. It has a flag isOpen that indicates the state of the accordion, a function toggle to change the state, there are two slots:

<script>
	export let isOpen = false
	
	const toggle = () => isOpen = !isOpen	
</script>

<slot name="header" {toggle} />

{#if isOpen}
	<slot />
{/if}

In order this accordeon you would place a button in the header slot and wire up the toggle function. All the styling of the button and the content is the responsibility of the consumer itself. In the example below we have added three such accordions:

<script>
    import { slide } from 'svelte/transition'
    import Accordion from './Accordion.svelte'
</script>

<div class="wrapper">
    <Accordion>
        <button slot="header" let:toggle on:click={toggle}>Ålesund</button>
        <img transition:slide src="/images/alesund.png" alt="" />
    </Accordion>
    <Accordion>
        <button slot="header" let:toggle on:click={toggle}>Trollstigen</button>
        <img transition:slide src="/images/trollstigen.png" alt="" />
    </Accordion>
    <Accordion>
        <button slot="header" let:toggle on:click={toggle}>Jotunheimen</button>
        <img transition:slide src="/images/jotunheimen.png" alt="" />
    </Accordion>
</div>

<style>
    div {
        display: flex;
        flex-direction: column;
        width: 400px;
    }
    button {
        padding: 1rem;
    }
    img {
        height: 300px;
        width: 400px;
    }
</style>

(Note that I added a transition to get a smoother feel to the accordeon)

And this is the result:

If you look close you will see that there is no styling or markup in the Accordion component itself. This means that the same component can be re-used elsewhere to make an accordion that looks drastically different, but keep the shared logic. This is true for all Renderless Components

Example 2: Notification

Another technique is to use the setContext and getContext api provided by Svelte.

setContext('my-context', variable) makes variable available to all descendants of a component through the context of ‘my-context’, to get this variable you just call `getContext(‘my-context’). There are some limitations as to when you call these methods, more information about that can be found in the Svelte Documentation.

Here the idea is to expose again two slots: one for the notification and one for all the children. The payload is the content to be shown in the notification and of course there are functions ‘hide’ and ‘show’. The slot with the notification will only be shown if payload is a true-like value.

<script>
	import { setContext } from 'svelte'
	
	let payload = false
	let timeout

	const hide = () => {
		timeout && clearTimeout(timeout)		
		payload = false
	}
	const show = val => {
		payload = val
		timeout && clearTimeout(timeout)
		timeout = setTimeout(hide, 5000)
	}
	
	setContext('notification-show', show)	
</script>

{#if payload}
	<slot name="notification" {hide} payload={payload} />
{/if}

<slot />

And the usage would be something like

<script>
    import Notifications from './Notifications.svelte'
</script>

<Notifications let:show>
    <div slot="notification" let:payload>{payload}</div>
    <!-- actual content goes here -->
    <button on:click="{() => show('this is a notification')}">Click here</button>
</Notifications>

For those familiar with React, this is very similar to the ProviderPattern that gives raise to these monstrosities:

<NotificationProvider>
    <ModalProvider>
        <ValidationProvider>
            <ContentProvider>
                <AuthorizationProvider>
                    <CustomerProvider>
                        {children}
                    </CustomerProvider>
                </AuthorizationProvider>
            </ContentProvider>
        </ValidationProvider>
    </ModalProvider>
</NotificationProvider>

In Svelte however we can make use of something called the ‘context module’. This a special script block that is common to all instances of a components and can be called from outside of a components. In the example below the notifications component was rewritten to use this, notice that there is no longer a need for a generic slot for the children, and that payload has to be a store (variables in the context module are not reactive by default.)

<script context="module">
	import { writable } from 'svelte/store'
	
	let payload = writable(false)
	const hide = () => payload.set(false)
	export const show = val => payload.set(val)	
</script>

{#if $payload}
	<slot {hide} payload={$payload} />
{/if}
<script>
    import Notification, { show } from './Notifications.svelte'
</script>

<Notification let:payload>
    <div>{payload}</div>
</Notification>

<!-- actual content goes here -->
<button on:click="{() => show('this is a notification')}">Click here</button>

This allows for much cleaner code.

A good use of the ‘ProviderPattern’

In the presentation I also give a good use of the provider pattern however, in a group of accordions you usually only want one to be open, this means that if one opens, the others have to be closed. The following code uses the ContextAPI to enable such behaviour by creating two components, one for the group as whole and one for the individual accordion

<script>
	import { setContext } from 'svelte'
	
	let current = false
	
	setContext('accordeon', {
		setCurrent: fn => {
			current && current !== fn && current()
			current = fn
		}
	})
</script>

<slot />
<script>
	import { getContext } from 'svelte'
	export let isOpen = false
	
	const close = () => isOpen = false
	const toggle = () => isOpen = !isOpen
	
	const { setCurrent } = getContext('accordeon')
	
	$: isOpen && setCurrent(close)
	
</script>

<slot name="header" {toggle} {isOpen} />

{#if isOpen}
	<slot />
{/if}

Renderless-Svelte as a Library

For some ready made components, you can check out renderless-svelte.dev a library I have developed that offers already a series of renderless components like Accordeons, Modals, Carousels, …