Last month I wrote about testing Svelte and Sapper to learn about their intricacies and limitations before trying to change my blog framework. I decided I'd explored enough to attempt a refactor and I found a few features that couldn't easily be replicated or done at all.
A .svelte file cannot export multiple components
The first problem I uncovered is a limitation within the Svelte framework. I found a workaround, but it was much, much more verbose.
On my Gatsby blog I use MDX to replace markdown elements with React components. Sapper has an equivalent tool, MDsveX. The configuration for both is similar — they need to import components and create an object that maps them to HTML elements.
// react
import { H1, H2, H3, H4, H5, H6 } from "../markdown/headings/index.js"
const components = {
h1: H1,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
h6: H6
}
<MDXProvider components={components}>...</MDXProvider>
// svelte
<script context="module">
import { h1, h2, h3, h4, h5, h6 } from './headings/index.js'
export { h1, h2, h3, h4, h5, h6 }
</script>
My goal is to replace each heading tag with a page link component. The component is build by taking the text content of the title and transforming it into a hash link. The hash is added as an id
to the heading and then wrapped in an <a>
tag that links back to the hash. This basically replicates the functionality of remark's autolink headers plugin.
MDX Headings in Gatsby
The React way to build this component pretty straightforward. Because I still need to generate 6 different headings I made a generic <Heading />
component that could accept a prop telling it which h
tag to render. I'll explain more about the slugify
function further down.
const Heading = (props) => {
import { slugify } from 'helpers'
const hashUrl = slugify(props.children)
const Level = `h${props.h}`
return (
<Level
id={hashUrl}
className='heading'
>
<a href={`#${hashUrl}`}>
{props.children}
</a>
</Level>
)
}
Then, in an index file, I was able to use the <Heading />
component to compose six new components, one for each of the HTML headings.
import Heading from './heading'
const H1 = props => <Heading h="1">{props.children}</Heading>
const H2 = props => <Heading h="2">{props.children}</Heading>
const H3 = props => <Heading h="3">{props.children}</Heading>
const H4 = props => <Heading h="4">{props.children}</Heading>
const H5 = props => <Heading h="5">{props.children}</Heading>
const H6 = props => <Heading h="6">{props.children}</Heading>
export { H1, H2, H3, H4, H5, H6 }
My directory structure looked like this.
└─ headings/
├─ heading.js // the generic component
└─ index.js // export of composed h1-h6
I want to emphasize that React is creating and exporting six unique components in a single index.js
file.
MDsveX Headings in Sapper
There are a few big differences with building this component in Svelte. Ignore the getText
function for now, I'll elaborate on that later.
<script>
export let h
import { slugify } from 'helpers'
let id
let hashLink
const getText = node => {
id = slugify(node.text)
hashLink = `#${id}`
}
</script>
{#if h === '1'}
<h1 id={id}><a use:getText href={hashLink}><slot/></a></h1>
{:else if h === '2'}
<h2 id={id}><a use:getText href={hashLink}><slot/></a></h2>
{:else if h === '3'}
<h3 id={id}><a use:getText href={hashLink}><slot/></a></h3>
{:else if h === '4'}
<h4 id={id}><a use:getText href={hashLink}><slot/></a></h4>
{:else if h === '5'}
<h5 id={id}><a use:getText href={hashLink}><slot/></a></h5>
{:else if h === '6'}
<h6 id={id}><a use:getText href={hashLink}><slot/></a></h6>
{/if}
The first is that Svetle doesn't seem to have the ability to use strings to dynamically choose html tags. Maybe this is possible with <svelte:component>, but I could only get this to work with other custom components and not html tags. The good news is that the Svelte community is aware of this, and at the time of writing this post there is an open proposal to implement a fix. For now I went with one of the easier fixes suggested in the GitHub issue and added a series of if else
statements.
The second issue lies in trying to replicate the index.js
file that exports six heading components.
import h1 from './h1.svelte'
import h2 from './h2.svelte'
import h3 from './h3.svelte'
import h4 from './h4.svelte'
import h5 from './h5.svelte'
import h6 from './h6.svelte'
export { h1, h2, h3, h4, h5, h6 }
Svelte components can be imported and exported using ES6 syntax, but only one component can be created per .svelte
file. Rather than naming a component and exporting it manually, Svelte will use the file name to automatically determine a component's named and how it is exported.
<script>
import Heading from './heading.svelte'
</script>
<Heading h='1'><slot/></Heading>
An index file that exports six headings is still possible, but the directory structure is a lot messier than with React.
└─ headings/
├─ h1.svelte // composed component
├─ h2.svelte
├─ h3.svelte
├─ h4.svelte
├─ h5.svelte
├─ h6.svelte
├─ heading.svelte // the generic component
└─ index.js // export of imported h1-h6
Sapper's router doesn't work with hash links
As with React, there is no officially supported Svelte router, only community favorites that most projects use. As far as I know Sapper doesn't use any one specific router package. Instead, it has a series of helper functions that handle internal navigation.
Sapper's routing strategy, however, does currently contain a pretty big bug. Clicking on <a>
tags that point to hash links will trigger a navigation to a new page rather than scroll to an id on the same page. A link on the www.site.com/example
page that looks like <a href='#test'>test</a>
should scroll the page to an element with id='test'
and change the url to www.site.com/example#test
. This link will mistakenly navigate the app to www.site.com/#test
, and likely end up rendering a 404 page.
This is a known bug
The Sapper team is aware of this bug with one or two issues already filed about it. I looked into the issue but couldn't figure out a fix since both the handle_click
and navigate
functions seem to have short circuits for when the urls contain hashes. Sapper's goto
function doesn't appear to have any way to know about a hash link, so maybe that's the issue?
There IS a temporary fix
There is a workaround for this bug.
<script>
import { onMount } from 'svelte'
onMount( () => {
document.querySelectorAll('a').forEach(a => {
if (!a.hash||!document.querySelectorAll(a.hash).length) {
return
}
a.href = window.location + a.hash
})
})
</script>
I am a strong proponent that all sites should work without JavaScript, so I don't love that this fix relies on the clientside onMount
lifecycle hook. On the other hand, if a user has all scripts blocked from running then Sapper won't be able to rehydrate in the first place. This means Sapper's router won't take over navigation and all links should keep their default browser behavior, so this is a non-issue. Still, this code takes a second to run after the component mounts and the links still momentarily don't work. Also depending on the CSS this could lead to some style flashes as href
shuffle around.
Components don't have programmatic access to their children
Time to circle back to the slugify
and getText
functions I mentioned earlier.
The HTML that MDX or MDsveX would replace looks like this:
<h1>The sun is shining, but the ice is slippery.</h1>
The goal is to transform the markup into something that looks like this:
<h1 id="the-sun-is-shining-but-the-ice-is-slippery">
<a href="#the-sun-is-shining-but-the-ice-is-slippery">
The sun is shining, but the ice is slippery.
</a>
</h1>
In the React version of the Heading component the id
and href
attributes are derived by running the child text of the component through a function that will make them viable hash links.
const Heading = (props) => {
import { slugify } from 'helpers'
const hashUrl = slugify(props.children)
return (
<h1
id={hashUrl}
className='heading'
>
<a href={`#${hashUrl}`}>
{props.children}
</a>
</h1>
)
}
This works because props.children
can be accessed and manipulated like any other React prop. Svelte's API works differently, providing the <slot />
component as a way to render child content.
<script>
import { slugify } from 'helpers'
let id
let hashLink
const getText = node => {
id = slugify(node.text)
hashLink = `#${id}`
}
</script>
<h1 id={id}><a use:getText href={hashLink}><slot/></a></h1>
props.children
and <slot />
are similar but have one major difference. React has access to the child content and can transform it during run time before render. Svelte needs to know about child content at compile time, and can't know about children until after they are rendered.
The only way I found to get the child content to pass to the slugify
function was to query it from the DOM after render. Svelte does have use:action
binding, which makes it very easy to run a function as soon as an element is created. There are two big downsides to this. First, this leads to the flash of no-link styles I mentioned earlier. A component is created, then once the page is loaded enough to fire a DOM query the hash link is generated and appended. The second is that since this relies on direct DOM manipulation, this function won't fire if a user doesn't allow Javascript. This is a bummer since hash links navigation is something that browsers support natively and definitely shouldn't require Javascript.
There is an open Pull Request that will fix this
Some good news about this missing feature is that there is currently an open pull request to add a $$slots
prop. This will work similarly to the existing $$props
value. It is hard to say if this would solve my exact problem since it isn't implemented yet, but any programmatic access to the child content should work better than having to query text after it renders. The bad news is that this feature doesn't currently appear on Svelte's roadmap so it's hard to say when exactly it might land.
Dealbreakers?
So, do these handful of "missing" features mean I don't recommend trying Svelte?
No, Svelte is still a great framework that has enough features to do most things. Since it is a little younger than some other frameworks and doesn't have a massive corporation behind it, it may not have features for a handful of fairly specific scenarios, yet.
This does, unfortunately, mean that I won't currently be moving my blog from Gatsby to Sapper. I plan on giving this another shot when Sapper does a full 1.0 release. This should give Svelte time to release more features and give me time to refactor some of my React code to make it more portable. I'm also in the early planning stages of another web app and I'm still considering using Svelte and Sapper to build it. Like I said, Svelte is great, it just isn't not a great fit for my blog right now.
I've also only been working with Svelte for a few months, and it is entirely possible that these things are doable and I just haven't discovered how. If that's the case I would absolutely love to be told I'm wrong, let me know @ryanfiller_ on Twitter and I'll update this article with the fix.