Writing a singleton for Vue.js composition API
Tags: vue, typescript, tutorial, webdev
Published: 2022-10-21 12:55:14

Vue.js [composition API](https://vuejs.org/api/composition-api-setup.html) gives developers the possibility to extract parts of components into dedicated modules. This approach can lead to cleaner and lighter components, more structured code and a simplified way to reuse code.

When a component becomes huge, it's quite simple to move parts of it into a module that will expose them to the outside world.

But how could we deal with a shared context between instances of the module? This is where singletons can be an answer.

## Expectations

To illustrate this, we'll go through the implementation of an onboarding feature:

- there is a list of messages to display
- a single message should be displayed at a given time
- we can cycle forwards & backwards through messages
- the onboarding can be globally shown/hidden
- the onboarding popups will be embedded in different components of the application

## Implementation

Our singleton will expose these elements:

```typescript
type OnboardingInstance = {                          
  show: Ref<boolean>;                                
  current: Ref<string>;                              
  prev: () => void;                                  
  next: () => void;                                  
};   
```

In our Vue components, we can operate on them the following way:

```vue
<template>
    <button @click="prev()">Prev</button>
    <span>{{ current }}</span>
    <button @click="next()">Next</button>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import useOnboarding from './onboarding.ts';

export default defineComponent({
  setup(props) {
    const { current, prev, next } = useOnboarding();
    return { current, prev, next };
  }
});
</script>
```

### First implementation

The code below implements the properties & methods that our module should expose. This is a simple Vue.js TypeScript module.

```typescript
import { computed, ref } from 'vue';

let step = ref(0);
let _steps: string[] = ['Step 1', 'Step 2', 'Step 3', 'Step 4'];
function prev() {
  if (step.value > 0) step.value--;
}

function next() {
  if (step.value < _steps.length - 1) step.value++;
}

const current = computed(() => _steps[step.value]);

return { show, current, prev, next };
```

There's no singleton here. Each component of our application will get its own instance of the module.

## Making it a singleton

To make this module become a singleton, let's wrap its code into a closure.

```typescript
export default (function () {

  // Paste the code of the first implementation here
  // (except the `return` statement of course)

  let instance: OnboardingInstance = { show, current, prev, next };
                            
  return () => {
    return instance;
  };
})()
```

There are important things to notice here:

- Our closure is executed automatically

```typescript
export default (function () {

})()  // <-- The JavaScript engine executes it because of the `()`
```

- It returns an anonymous function that returns the instance

```typescript
  return () => {
    return instance;
  };
```

### Making it able to get arguments

Our module is now a singleton. Steps & state are shared across all the components that load it. But steps are hard-coded into the module. Let's fix this now.

The anonymous function that we return is the one that will get the arguments from the outside world: 

```typescript
  return () => {
    return instance;
  };
```

To be able to declare the steps from the outside, we can change it this way:

```typescript
  return (steps?: string[]) => {
    if (steps) {
      _steps = steps;
    }
    return instance;
  };
```

Then pass the steps from the component to the singleton:

```typescript
const { current, prev, next } = useOnboarding(['Step 1', 'Step 2', 'Step 3', 'Step 4'])
```

### Final thoughts

The main difficulty of this pattern is with preserving Vue.js reactivity.

This implementation preserves Vue.js reactivity provided you get its props & methods through [destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment).

The use of the instance as a single object with preserved reactivity is possible through the [reactive](https://vuejs.org/api/reactivity-core.html#reactive) method of Vue.js:

```typescript
const onboarding = reactive(useOnboarding());
```

---

This singleton implementation gives the possibility to maintain a shared state and have multiple components subscribe to its data and render it. This way the components code itself can be lighter and easier to deal with.