im

Svelte's module scripts explained

Learn everything about Svelte's module script blocks and how they work

Svelte module script blocks have always been little mysterious to me. Sure, I use them all the time, but I never actually took the time to learn how they work.

Today I finally decided to go to the bottom of this. What I found was surprising, but I was actually not that surprised. Maybe it's because this is exactly how I imagined them to work?

Anyhow, turns out it's very simple and the Svelte compiler does no magic at all! Read on to learn how Svelte's module scripts work!

Note: This article is code-heavy. It's needed in order to clarify things. The code examples themselves are however kept light. No need to complicate things to get the point across.

Getting the basics down#

You have probably stumbled upon module functions if you've worked with Sapper. This example is taken straight from its documentation.

<script context="module">
export async function preload(page, session) {
const { slug } = page.params;

const res = await this.fetch(`blog/${slug}.json`);
const article = await res.json();

return { article };
}
</script>

<script>

export let article;
</script>

<h1>
{article.title}</h1>

How does it work and how is the article available to the component? Well, in case of Sapper there is some magic going on behind the scenes, but to explain it in simple terms:

Everything you define in a module script block will be available to the component defined in the same file.

Let's break it down and start with the basics. First, we create a simple Svelte component that has a module script block. In order to understand what's going on we will add a few logging statements.

<!-- Component.svelte -->

<script context="module">

console.log('init module');

let foo = 'bar';
const settings = { show: true };
</script>

<script>

import { onMount } from 'svelte';

export let number = 0;

console.log('init component');

onMount(() => console.log(`mounted component #${number}`));
</script>

<div class="component">
<h3>Component #
{number}</h3>
<p><code>module[Component].foo =
{foo}</code></p>
<p><code>module[Component].settings =
{JSON.stringify(settings)}</code></p>
</div>

In the module block we defined two variables, foo and settings. Then, in the component itself we access those variable just like we define them in the component itself.

How is that possible? If you look at the Javascript file Svelte compiler generated for us you will see that the things we defined in the module block were copied to the main module file.

// Component.js

console.log("init module");

let foo = "bar";
const settings = { show: true };

function instance($$self, $$props, $$invalidate) {
let { $$slots: slots = {}, $$scope } = $$props;
validate_slots("Component", slots, []);
let { number = 0 } = $$props;
console.log("init component");
onMount(() => console.log(`mounted component #${number}`));
const writable_props = ["number"];
// ...
}

class Component extends SvelteComponentDev {
constructor(options) {
super(options);
init(this, options, instance, create_fragment, safe_not_equal, { number: 0 });

dispatch_dev("SvelteRegisterComponent", {
component: this,
tagName: "Component",
options,
id: create_fragment.name
});
}
// ...
}

export default Component;

All the compiler does is to copy over anything defined in the module block to the top of the Javascipt module file where component is defined.

Since our Svelte component lives in the same module file, everything defined in the module context is automatically available to the component. That's how Javascript scopes work.

<!-- index.svelte -->

<script>

import Component from './Component.svelte';
</script>

<Component number=
{1} />
<Component number=
{2} />
<Component number=
{3} />

When we import the component into our index.svelte file it will import the component class itself because it's a default export from the Javascript module Component.js.

Since component is defined in that module the module will be parsed and executed by the Javascript engine and all module level variables will be initialized.

import Component from "./Component.js";

When we view this page in the browser with dev tools open we will see this output in the console.

Svelte module context execution

As you can see from the screenshot the module is executed only once, and also before any component is initialized. It happens when this module is imported by some other file.

When variables and functions are defined on the module scope they will be evaluated only once, when the module is first evaluated (imported), and will be available to all components defined in that module.

As of today, you can only have one component per module in Svelte, but there is an RFC to be able to define multiple components per module, eg in one Svelte file. Maybe we will be able to in Svelte v4.

Hopefully this make it more clear how module level scripts work, but it's not very useful right now because we cannot manipulate these module level variables. Let's fix it.

Making it useful#

In order for us to be able to change our shared variables, and for those changes to be picked up by all components, the variables have to be reactive.

Let's convert them to Svelte stores instead.

<!-- Component.svelte -->

<script context="module">

import { writable } from 'svelte/store';

const foo = writable('foo');
const settings = writable({ show: true });
</script>

<script>

import { onMount } from 'svelte';

export let number = 0;
onMount(() => console.log(`mounted component #${number}`));
</script>

</style>

<div class="component">
<h3>Component #
{number}</h3>

<div class="flex mt">
<code>module[Component].foo =
{$foo}</code>
<button on:click=
{() => ($foo = `Component #${number}`)}>change foo</button>
</div>

<div class="flex mt">
<code>module[Component].settings =
{JSON.stringify($settings)}</code>
<button on:click=
{() => ($settings.show = !$settings.show)}>toggle settings.show</button>
</div>
</div>

This is what the compiled component file looks like now.

// Component.js

// ...
const foo = writable("foo");
const settings = writable({ show: true });

class Component extends SvelteComponentDev {
// ...
}

export default Component;

Now we can manipulate those variables from any component and all the changes will be instantly reflected in the other components. The power of Svelte's reactivity.

Module exports#

But what if we want to manipulate and access these module variables outside the component itself? We can do that by prepending an export statement to the variables and function we want to export.

<!-- Component.svelte -->

<script context="module">

import { writable } from 'svelte/store';

// export foo store from module
export const foo = writable('foo');
// this store is private, not exported from the module
const settings = writable({ show: true });

export const setFoo = s => {
foo.set(s);
};

// export function to manipulate module state that is private to the module
export const toggleShow = () =>
settings.update(state => ({ ...state, show: !state.show }));
</script>

<script>

import { onMount } from 'svelte';

export let number = 0;

onMount(() => console.log(`mounted component #${number}`));
</script>

<div class="component">
<h3>Component #
{number}</h3>
<p><code>module[Component].foo =
{$foo}</code></p>
<p><code>module[Component].settings =
{JSON.stringify($settings)}</code></p>
</div>

If you look in the end of the compiled file you will see that the Svelte compiler exports these variables for us along side with the default Component module export.

// Component.js

const foo = writable("foo");

// this store is private, not exported from the module
const settings = writable({ show: true });

const setFoo = s => {
foo.set(s);
};

const toggleShow = () => settings.update(state => ({ ...state, show: !state.show }));

class Component extends SvelteComponentDev {
// ...
}
export default Component;

export { foo, setFoo, toggleShow };

This means we can import our exports in other places like so.

<!-- index.svelte -->

<script>

import Component, { toggleShow, setFoo, foo } from './Component.svelte';
let value = '';

const setValue = () => {
setFoo(value);
value = '';
};
</script>

<div>
<code>foo =
{$foo}</code>
<input type="text" placeholder="New foo value" bind:value />
<button on:click=
{setValue}>set foo value</button>
<button on:click=
{toggleShow}>toggle show outside component</button>
</div>

<Component number=
{1} />
<Component number=
{2} />
<Component number=
{3} />

If we look at the compiled index.svelte file we will find the following import statement.

// index.js

import Component, { toggleShow, setFoo, foo } from "./Component.js";

This is pretty much everything you need to know about Svelte's module script blocks.

DIY context="module"#

If you for some reason don't want to use module scripts you achieve the same functionality with pure Javascript modules. Here is a simple example of how to do it.

<!-- Page.svelte (former index.svelte) -->

<script>

// import module state and Svelte components from main module file
import { Component, toggleShow, setFoo, foo } from './index';

let value = '';

const setValue = () => {
setFoo(value);
value = '';
};
</script>

<div>
<code>foo =
{$foo}</code>
<input type="text" placeholder="New foo value" bind:value />
<button on:click=
{setValue}>set foo value</button>
<button on:click=
{toggleShow}>toggle show outside component</button>
</div>

<Component number=
{1} />
<Component number=
{2} />
<Component number=
{3} />

We will import our module variables from the index.js file.

<!-- Component.svelte -->

<script>

import { onMount } from 'svelte';
// import module state stores
import { foo, settings } from './index';

export let number = 0;

onMount(() => console.log(`mounted component #${number}`));
</script>

<div class="component">
<h3>Component #
{number}</h3>
<p><code>module[index].foo =
{$foo}</code></p>
<p><code>module[index].settings =
{JSON.stringify($settings)}</code></p>
</div>

We move our module variables from Component.svelte to the index.js module.

// index.js

import { writable } from 'svelte/store';

// create and export shared state stores
export const foo = writable('foo');
export const settings = writable({ show: true });

// export helper functions
export const setFoo = s => foo.set(s);

export const toggleShow = () =>
settings.update(state => ({ ...state, show: !state.show }));

// export Svelte components
export { default as Component } from './Component.svelte';
export { default as Index } from './Page.svelte';

In the compiled Javascript version for Page.svelte file you will see the following import statement.

// Page.js

import { Component, toggleShow, setFoo, foo } from "./index.js";

And in the Javascript version of Component.svelte you will find this statement.

// Component.js

// ...
import { foo, settings } from "./index.js";

class Component extends SvelteComponentDev {
// ...
}

export default Component;

As you can see, we managed to replicated Svelte's module block functionality with regular Javscript. I don't know if we gained anything from it though. Feels like it only complicates things for us.

Summary#

The context module directive is straight-forward when you examine it closely. When you tag your script with context="module" the code it contains will be copied to the module level of that Svelte file. That's it. How variables and functions defined on the module level can be accessed in the component class is just how JavaScript scoping works.

You can find the full code for this article if you want to play around with it.

https://github.com/codechips/svelte-module-scripts-examples

One might wonder why there's such trivial, and even unnecessary, functionality in Svelte as module level blocks. Modules and Svelte slots allow us to build very powerful abstractions on top of them. I will teach you how in my future posts.