I Want to Start Using single-spa: The Basics
Tags: singlespa, svelte, tutorial, beginners
Published: 2023-05-01 00:00:50

> Heredia, Costa Rica
> 
> Published on 2023-04-30

Hello! I haven't written anything for a long time due to other obligations, but let's see if I can attract your interest to this new series.

It all started with me wanting to move (at work) our current custom micro-frontend project to something consolidated: `single-spa`. However, because I cannot blindly start this migration, I need to run some testing and make a few Proof Of Concept projects.

We will put `single-spa` to the test on various fronts in order to satisfy my work needs: Does it work with my modified React micro frontends? Can I make React play nicely with Svelte? Does Svelte routing interfere with React routing? All these and other questions will be probed for an answer.

This series will cover my journey from learning the basics of `single-spa`, all the way to using it to convert existing CRA-created **React** projects, but especially with my eye on the horizon, where **React** will be replaced with **Svelte**.

> **Disclaimer**: I may not be able to reveal certain things in the blog, so expect some replacements here and there in future articles.

## So What is This About?

`If (this === 'this article') then` today we are covering the basics:

* Core concepts of `single-spa`
    
* The tools that `single-spa` provides, and why not use them
    
* Example code for a root project and a mife, both in **Svelte**
    

The progression of the learnings will be available in upcoming articles in the series.

`Else if (this === 'this series')` then it is all about documenting my journey of migrating from a custom micro-frontend solution to `single-spa`. Why is this interesting? Well, you'll be the judge of that. What I can tell you is that I will be learning how to use `single-spa` without pre-made template projects or other utilities (`SystemJS`, most notably). I will try to explore its details and unravel its power on my own, without pre-made recipes as long as it makes sense to me and my goal.

As an example, I will be teaching you how to make a better mife project that allows standalone execution out of the box, as opposed to what you get with `create-single-spa`. This tool creates mife projects that, by default, don't show your mife. I dislike this dearly. Say No to *"Your Microfrontend is not here"*!

Bookmark [the series](https://webjose.hashnode.dev/series/single-spa-and-svelte) now so you don't miss a thing!

With that said, let's get started.

## single-spa 101..201

`single-spa` is a framework that makes it easy for you to integrate "small applications" (mife, or micro-frontend) into one, larger application.

How does `single-spa` do what it does? By providing:

* An interface definition all mifes must adhere to.
    
* An API around said interface that provides:
    
    * Registration of mifes
        
    * The ability to define when a mife should be live and when it should not
        
    * Data and code sharing between the root and the mifes
        
    * Control over when to start mife lifecyles
        
    * A set of events fired at the window level to inform when things happen
        
* Utilities to test or enhance the application, most notably the `navigateToUrl()` function which can be used to easily make a client-side router
    

> Hopefully I'm not missing anything important from that list!

Let's have a look at this interface definition that makes it all possible.

### Mife Lifecycle

The one requirement for a mife to be a `single-spa` **mife** is to implement, as a minimum, the following 3 functions:

* `bootstrap`
    
* `mount`
    
* `unmount`
    

Said functions must be made directly available as named exports of your mife's main module. Does this confuse you? Maybe not, but just in case, let's also cover this.

It is expected (and don't worry, as this is the norm nowadays) that your mife application be bundled into (ideally, but not necessarily) a single ES module that fulfills the aforementioned exports. I'll show you some example code of how this looks like with a **Svelte** component that encapsulates what the micro-frontend wants to expose:

```typescript
import MainComponent from "$lib/MainComponent.svelte";

export function bootstrap (spaData) {
    // Bootstrap code here.
}

export function mount(spaData) {
    // Mount code.  This is where MainComponent gets rendered into a spa-provided element.
}

export function unmount(spaData) {
    // Unmount code.  Undo what mount() did.
}
```

The above example would be your bundle's entry file, say named `spa.ts`. You would specify this file as input to your bundler, most famously, `webpack` or `rollup`. When you build your application, the bundler will spit out your bundled module, ready to be consumed by `single-spa`.

Ok, with the mife requirement out of the way, let's see how `single-spa` defines the root project and some considerations around it. How the root project registers and dynamically mounts and unmounts mifes will follow.

> `single-spa` uses the term "root-config" or "root config" to refer to the project that contains `single-spa` and is in charge of loading the micro-frontends. I don't like this, so I use "root project".

### Root Project

The root project is your application's entry point. It is the HTML code that gets delivered to your customers when they visit the application's domain name.

The developer team from `single-spa` recommends that you don't use any framework in your root project, and instead create an empty shell that merely loads mifes (Micro-Frontends). You can read what they have to say [here](https://single-spa.js.org/docs/faq/#should-i-have-a-parentroot-app-and-children-apps).

I have thought about this and essayed the scenario in my head, keeping my context in mind: I will end up with a project that does little, and requires a pod in K8s (I use K8s at work) to serve a single JS file, a single HTML page and one CSS file that sets the common elements of the website's style, such as font, background, body size and so on. Then this page will run the JS and load the application's shell containing the header and navigation menu, which in turn provides the placeholders where other mifes are loaded. Yes, the root project will contain most of the `single-spa` code (registration of all mifes), but the shell has to define the positions on the page where the mifes are mounted.

To avoid having to update 2 projects every time a mife is added or removed, the shell application should register the other mifes so the root application doesn't have to be updated if the mife list in the shell side grows or shrinks. `single-spa` accounts for this: As part of the data that `single-spa` transmits to mifes, the `single-spa`'s main instance is transmitted so mifes can register other mifes.

So far, I get what they say on the referenced page about separation of concerns, and I think my purist side agrees with them, but my practical side is yelling at me: "Are you seriously considering creating a project that will most likely sit there for years untouched?" Because let's think about it: What if my shell project acted as root project? Yes, it will have a reason to change other than changes in the header/menu: It now has to change because I add or remove a mife, or if something else, `single-spa`\-related, changes. Sounds SOLID-breaking, for sure. However, if I go the `single-spa` way, I will have an almost-empty project that does one thing: Load the shell. Do I want an entirely separate project for this, practically speaking? I am leaning towards "No, I don't want this."

Let's imagine for a moment the contents of the root project. It would be this:

* One HTML file
    
* One JavaScript file/bundle
    
* One CSS file/bundle
    

The HTML imports the CSS, then runs the JavaScript. The JavaScript merely registers and loads the Shell. That's it. That's 100% of the root application because other mifes are registered and positioned by the shell itself, not the root project.

Now let's ask us this question: What does it take to move the root application's functionality inside the shell mife? Answer: Move the CSS to the shell mife, and have the shell mife create the HTML page that imports said CSS. This is the simplest thing in the world. I am now sure: I won't create a root application that isolates `single-spa`. I see no practical benefits whatsoever, at least at this stage in my learning process.

Now, all this time I have been reasoning this with my work application in mind, which consists of a shell and several microservices living as hierarchical children of said shell. The shell sits on top, always. Would my ruling change if I were to remove this constraint from my head? Let's see.

I believe that I would consider following `single-spa`'s team's recommendation only if I had more than one top-level mife. Still, I bet any multi-top-level-mife application can be imagined as a single-top-level-mife application, which would then rule out `single-spa`'s recomendation, again, as per the previous reasoning.

My conclusion: Don't follow `single-spa`'s recommendation. Write your shell on your favorite framework (**Svelte**, **Vue**, **Preact**, **React**,... did I say **Svelte**?), and add the `single-spa` functionality on top of it to make it your root project.

Of course, feel free to draw your own conclusions and post them in the **Comments** section so we all benefit from each other!

### Registering and Mounting Mifes

Usually, the root project is in charge of both registering and mounting mifes. However, as explained in the previous section, `single-spa` allows mife registration from within mifes.

We use the `registerApplication()` function of `single-spa` to define, or register, mifes. This has to be done in a JavaScript module that always runs, regardless of the URL being loaded. Remember that even in SPA's, a user may type a URL that has a route in it, and then you must ensure that mife registration always run, regardless of the URL. Where? It depends on your root project's framework. For example, in **SvelteKit**, we could use `src/routes/+layout.svelte` because all routes usually inherit from it. If you think you could use `src/routes/+page.svelte`, then you'd be wrong because a user may start your application by typing the URL with a route other than the homepage, and then registration doesn't happen because the user didn't start his session on the homepage, effectively bypassing `src/routes/+page.svelte`.

Let's see some code:

```typescript
import { registerApplication } from "single-spa";
import someGlobalConfigurationObject from "./global-config.js";

registerApplication({
    name: 'mife-A',
    app: () => import('@scope/mifea'),
    activeWhen: '/',
    customProps: {
        globalConfig: someGlobalConfigurationObject
    }
});
```

First, we import `registerApplication()`. The second import is meant to be an example of a piece of data that we have available in the root project, and that we want to share with the registered mife. This is not part of `single-spa`, and it is completely optional.

Then we see the call to `registerApplication()`. It takes a configuration object with the four properties seen in the example: `name`, `app`, `activeWhen` and `customProps`.

The `name` property is an identifier we give to the mife. No other mife can be registered with a previously-used name.

> This will come into play in an upcoming article. It will be used to enhance HMR (hot module reload) and probably to be able to use SvelteKit's SSR feature.

The `app` property is the main course: A function that returns an object with the lifecycle functions we talked about earlier: `bootstrap`, `mount` and `unmount`. Instead of a function that returns this object, it can be the object itself, if you have it ready for some reason. However, the most common thing to see here is a function that makes a dynamic import of the mife's module.

> Are you wondering about the module's name (`@scope/mifea`)? This has to do with import maps, discussed later.

The `activeWhen` property is the dessert: It lets us control when **mife-A** will be active and when it won't be. It can be a string, in which case it is treated as a URL location route prefix; it can be a function that receives the value of `window.location` in its first parameter and is meant to return `true` or `false`; or it can be an array with a mixture of the two aforementioned cases. The example shows the simplest: "The mife mife-A will be active always" because the route prefix `"/"` is well, always there for any URL.

Finally, the `customProps` property is the cherry on top of the dessert: You may pass data from the registrant of the mife (usually the root project) to the mife itself using this property, which is an object containing anything you want. In the example, we are passing some object that represents configuration data meant to be shared amongst all mifes.

### Starting Mife Loading

This is the simplest thing in the world to do. Whenever your application is ready to allow mifes to dynamically load and unload, just call `start()`:

```typescript
import { start } from "single-spa";

registerMifes();
// Do any other pre-work required.  Then fire away!
start();
```

You are not required to immediately start `single-spa` after mife registration. Feel free to call `start()` whenever is convenient for your application.

We are almost done with the basics. Let's talk about import maps.

### Import Maps

There is this thing I never knew about until recently: Import maps are "dictionaries" that define, or map, "bare identifiers" to ES module URI's. It is a browser feature that, at the time of this writing, all major browsers support fully.

One defines an import map using a `<script>` tag with type `importmap`. It is placed inside the HTML's `<head>` element, before any JavaScript is loaded. Let's see the import map that would satisfy the previous example where we wrote `import('@scope/mifea')`:

```xml
<!DOCTYPE html>
<html>
    <head>
        <title>Fun with single-spa</title>
        <script type="importmap">
            {
                "imports": {
                    "@scope/mifea": "http://localhost:3100/src/spa.ts"
                }
            }
        </script>
        <script type="module" src="/assets/index.js"></script>
    </head>
    <body>
        <div style="display: contents;">
            ...
        </div>
    </body>
</html>
```

This map tells the web browser: "If you see imports in JavaScript that say `from "@scope/mifea"`, know that `@scope/mifea` is really `http://localhost:3100/src/spa.ts`". This also applies to dynamic imports.

This level of indirection is so handy, that it can even be used to replace individual mifes in the browser, while the application is running! This is the recommended technique for development using `single-spa`: Open your application from a deployed environment, then override the import map to substitute whichever mife you are developing/testing and have fun. This way we don't have to run several projects locally on our machines just to develop or test changes.

---

Ok, that was a lot for just "the basics". Still, I believe it is useful. This is the basics summary:

1. Create your root project using your favorite UI framework or library; there is no practical gain in creating a pure root.
    
2. Create mife projects as needed, again, using your favorite UI framework or library.
    
3. Register all mifes using `registerApplication()` in the root project, in a JavaScript module that is guaranteed to run regardless of the URL.
    
4. Start the fun calling `start()` in the root project whenever ready.
    
5. Optionally share code or data in the root project with the mifes at the time of registration.
    

## Getting Started with create-single-spa

As seen in other JavaScript frameworks/libraries, `single-spa` also provides a tool capable of bootstrapping your root or mife projects fast and pain-free. Or is it, really, pain-free? We'll find out soon enough. Let's learn about this `create-single-spa` tool.

### Creating the Root Project

This `create-single-spa` NPM package is installed globally and then run in the command prompt. To create a root project, run this:

```bash
create-single-spa my-root --moduleType root-config
```

Just like that, the tool will create the `my-root` subdirectory, and in it, a plain JavaScript project ready for you to add import maps and calls to `registerApplication()`.

However, if you are like me and concluded that a root project that does nothing but loading SPA's is a waste of effort, then this is **not for you**. Unlearn this!

### Creating Mifes

The same `create-single-spa` tool can be used to create mifes. For example, you could start a new project for a **Vue** mife like this:

```bash
create-single-spa my-vue-mife --framework vue
```

Note that it is not necessary to specify `--moduleType app-`

because it is inferred by the tool based on the recommendation (that I don't share so far) of only using frameworks in mifes.

I tested this but unfortunately, at the time of this writing, there is a problem with the generated `package.json` file because running `npm i` fails with the following:


![NPM Install Error for Svelte parcel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wf7ie9qdyzj2gwvtr8p9.png)

Maybe I should have tried another framework (this failure was with Svelte). Still, we can have a look at what it creates. Let's start with the list of files:

![Project files for a parcel project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pop16b3tlu4bhrzlk4qt.png)

How do these relate to what we have discussed so far? We can see about that. Keep going.

First, we have `App.svelte`. This is the main component and the one component that would first appear when `single-spa` mounts this mife. When does this happen? Well, whenever the activity function(s) associated with this mife returns a `true` value.

Then we have `testorg-test-mife.js`. This name comes from my input when running `create-single-spa`: I was asked for an organization name and a project name, to which I replied `testorg` and `test-mife`. This file contains the export of the mife's lifecycle functions `bootstrap`, `mount` and `unmount`. This is the file content:

```javascript
import singleSpaSvelte from "single-spa-svelte";
import App from "./App.svelte";

const svelteLifecycles = singleSpaSvelte({
  component: App,
});

export const { bootstrap, mount, unmount } = svelteLifecycles;
```

The `single-spa-svelte` module comes from an NPM package of the same name. It is a helper package that creates the lifecycle functions with very little input. It is pretty handy, and so far, I approve of its use, if you still care about my personal opinion.

Finally, let's talk about `rollup.config.js`. This project uses `rollup` for code bundling. This file is extensive and I am no `rollup` expert (yet), so let's focus our attention on this piece:

```javascript
  input: "src/testorg-test-mife.js",
  output: {
    sourcemap: true,
    format: "system",
    name: null, // ensure anonymous System.register
    file: "dist/testorg-test-mife.js",
  },
```

Just like I stated earlier in the article, we are asking `rollup` to use the JavaScript file that exports the lifecycle functions as `input`. The `output` part of the configuration tells us that the output format will be a `SystemJS` module, and the destination file will be `testorg-test-mife.js` in the `dist` folder.

> `SystemJS` is an alternative to native import map support. It is the preferred import map solution recommended by `single-spa`, and it is therefore one reason why I **personally** don't recommend creating mifes with `create-single-spa`. Still, you can make your own mind yourself. If you are OK with `SystemJS`, by all means, continue.

Besides defaulting to `SystemJS` import maps, I see the following as issues:

1. The project created is not in **TypeScript**
    
2. I don't have **Vite**
    
3. The project is not **SvelteKit**
    

The last one is particularly important for me because I like very much the routing system of **SvelteKit**. Who am I kidding? **Vite** is also important to me. I love **Vite**.

At this point, I can either struggle with this tool to make it make projects the way I like them, or I can ditch it altogether and manually set the mifes up. I'm going for the latter option and ditching `create-single-spa`.

There's also one other point of interest for me: Can `create-single-spa` update my existing **React** mifes flawlessly? I bet not because they already are custom projects to be rendered with our proprietary SPA mechanism.

---

At this point, we are left with no helpers to build root or mife projects, but fear not, as `single-spa` itself is not very difficult. Let's learn how to make these ourselves.

## Getting Started With ... Nothing

The section's title is a bit harsh, but in reality, is a breeze. Let's jump right in using my favorite framework, **Svelte**.

### Creating the Root Project

I want to re-write my shell (currently in **React**) in **Svelte**, so I want my root project to be a **SvelteKit** project. So let's do that:

```bash
npm create svelte@latest
```

This starts the text-based wizard. I just ran it to show you a screenshot of the output:

![SvelteKit's output](https://cdn.hashnode.com/res/hashnode/image/upload/v1682823494555/348569fc-c8b5-41bc-a8c1-9d8ff7c0aca2.png align="center")

Now we need to run `npm i` and then add `single-spa`:

```bash
npm i && npm i single-spa
```

We are ready to register mifes, if we had any. No issues. We can leave the project ready for mifes in the meantime. Let's add the `src/routes/+layout.svelte` file with the following code:

```typescript
<script lang="ts">
    import { registerApplication, start } from "single-spa";

    // Register mifes.
    registerApplication({
        name: "",
        app: () => import(""),
        activeWhen: () => true,
        customProps: {
            // No idea.
        }
    });

    // Start single-spa!
    start();
</script>
```

And just like that, we have a `single-spa` root project with all the benefits of **SvelteKit**, including its nice router, which is what interests me the most for my work application. Sure, all is empty now, but we will come back and fill in the name and module alias as soon as we decide on them.

> In reality, a few more things are needed to call this root project "mife ready". Read the example walkthrough in the coming sections.

### Creating Mifes as New Projects

The easiest for sure is to create a mife from scratch. For my work application, everything is in **React**, and for now, I cannot afford to leave **React** behind in the mifes. I will only be re-writing the shell for now.

Still, this will most likely be the process to create a **Svelte + Vite** mife project, once I can afford the migration:

```bash
npm create vite@latest
```

This is what I fed the wizard with, and the corresponding output:


![Vite + Svelte project creation output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dtwh9a3xntcbg263eou2.png)

To make this a `single-spa` mife, we have to do some more work when compared to what we did for the root project. Still, it is nothing out of the ordinary.

First, open `src/main.ts`. This is the project's input file (the file that gets bundled by `rollup`). We could modify it to make this project a pure `single-spa` mife, but instead let's leave it untouched because, in a later article, I will show you how to make this project behave dually: As a regular standalone web application and as a `single-spa` mife.

Add the helper package named `single-spa-svelte`:

```bash
npm i single-spa-svelte
```

Then create a new input file: `src/spa.ts` with the following content:

```typescript
import App from "./App.svelte";
import singleSpaSvelte from "single-spa-svelte";

const lc = singleSpaSvelte({
    component: App
});

export const { bootstrap, mount, unmount } = lc;
```

Now we need to tell `rollup` that this is our new input file. Open the `vite.config.ts` file and modify it so it looks like this:

```typescript
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [svelte()],
    build: {
        target: 'es2022',
        rollupOptions: {
            input: {
                spa: 'src/spa.ts'
            },
            preserveEntrySignatures: 'exports-only',
            output: {
                exports: 'auto',
            }
        }
    },
})
```

We are telling **Vite** to target a very modern JavaScript version to allow for **top-level awaits**, we are changing the input to our new `spa.ts` file, and we are also asking rollup to please leave our exports exported.

At this point, we should have a live mife! Build the mife project with `npm run build`. You should get something very similar to this:

![Parcel build output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9yy34usx8e0adl5xn1b6.png)

Run the server so we can test it out as explained in the next section. Just execute `npm run preview`. Make a note of the hostname as it is needed for the next section. It probably is `http://localhost:4173`.

### Putting Root and Mife Together

Now we have a working mife, so let's head to the root project and register it properly. Make the contents of `src/routes/+layout.svelte` look like this:

```typescript
<script lang="ts">
    import { registerApplication, start } from "single-spa";

    // Register mifes.
    registerApplication({
        name: "mife01",
        app: () => {
            const module = "@test/mife01";
            return import(/* @vite-ignore */ module);
        },
        activeWhen: '/m01'
    });

    // Start single-spa!
    start();
</script>

<slot />
```

We now need an import map in the HTML page that maps `@test/mife01` to the bundled version of `src/spa.ts` in our mife project. Open `src/app.html` and add the import map. The result should look very similar to this:

```xml
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<link rel="icon" href="%sveltekit.assets%/favicon.png" />
		<meta name="viewport" content="width=device-width" />
		<script type="importmap">
			{
				"imports": {
					"@test/mife01": "http://localhost:4173/assets/spa-11fb199e.js"
				}
			}
		</script>
		%sveltekit.head%
	</head>
	<body data-sveltekit-preload-data="hover">
		<div style="display: contents">%sveltekit.body%</div>
	</body>
</html>
```

The right-hand value `http://localhost:4173/assets/spa-11fb199e.js` is the result of concatenating the host of the mife project server and the path of the bundled spa JavaScript file obtained when we built the mife project.

Now, because we want the **mife01** mife to be active if the URL has the path segment `/m01`, let's add that route to the root project. Create the folder `src/routes/p01`, and in it, the `+page.svelte` file. The contents of this file could be something like this:

```xml
<h1>Mife 01 Below</h1>

<p>
    This is content placed by the root project. Below this text, 
    you should see the contents of the mife01 mife.
</p>
```

Now let's place an anchor element in the root project's homepage, just to facilitate testing. Open `src/routes/+page.svelte` and add the anchor element shown below, in the last line:

```xml
<h1>Welcome to SvelteKit</h1>
<p>
    Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
</p>
<a href="/p01">Let's see Mife 01!</a>
```

We are theoretically ready. There is one more detail, though, that seems to be specific to **Svelte's SSR** (Server-Side Rendering). Long story short, **SSR** and `single-spa` are not friends right away. For the time being, just disable **SSR**. Create the file `src/routes/+layout.ts` with this content:

```typescript
export const ssr = false;
```

Ok! Run the root project with `npm run dev` and open a web browser to look at it. Click the **Let's see Mife 01!** link. You should see Mife01 now!

Hmm, but it doesn't look right now, does it? The **Vite** and **Svelte** logos are missing, and all styling has been lost. Fixing these is the subject of subsequent articles, and at this point, we should be happy about our progress. Pat yourself in the back!

## Conclusions

We have learned that `single-spa` is a powerful resource that is simple to use, albeit not trivial. The `create-single-spa` utility appears to be hopeless: It is opinionated (forces JavaScript even if you want TypeScript, and there's no Vite) and probably can only update existing projects that conform to out-of-the-box configurations. I, however, did not test the update capabilities, making this a *suspicion*, not a fact.

At this point, `SystemJS` seems completely unnecessary and an extra dependency that cannot be justified. When dealing with existing applications, the fewer extra dependencies, the better.

There is no practical gain in creating a bare-bones root project, and instead, it seems to be better just to make the shell project a root project by adding `single-spa` to a module in the shell that is guaranteed to always run.

`single-spa` is neither **SSR**\-friendly nor **HMR**\-friendly out of the box.

Manually creating mifes has the effect of losing CSS styles; extra work is needed to be able to serve assets like images from mifes.

---

### Upcoming Articles

In no particular order, we will explore how to fix CSS and how to regain access to assets like images. We will also learn how to create a mife project that simultaneously works as a `single-spa` mife and as a standalone web application.

We will also be testing the limits of `single-spa`: Can it really share code and data between mifes? Does mife routing really work out of the box? For example, can a **React** mife with **React** routing work flawlessly inside a **SvelteKit** shell?

Don't forget to follow me if you would like to see what happens next. Once more, here's the link to [the series](https://webjose.hashnode.dev/series/single-spa-and-svelte).

Happy coding!