We’ve recently been discussing various ways to manage state in a StencilJS application, including using Stencil State Tunnel and Redux. We’ve specifically been considering the context of building Ionic applications built with StencilJS, but these concepts apply to any StencilJS application.
We might implement some kind of “state management” solution to keep track of things like:
- Settings values that can be accessed throughout the application
- A logged in user’s data
- Posts, articles, comments etc.
However, some applications may benefit from a simpler solution rather than a full-featured state management solution like Redux. If you have a background with Angular then you may be used to using services/providers/injectable to fulfill this role. Angular has the concept of an @Injectable
built-in, which we can use to create a service that can share data/methods throughout our application.
StencilJS is not as “opinionated” as Angular - remember that whilst StencilJS can afford us a lot of the benefits of a traditional framework, it isn’t actually a framework (it is a web component compiler) and it doesn’t have a bunch of the built-in features that Angular does.
Although this kind of functionality isn’t baked into StencilJS, it is actually quite straight-forward to implement with vanilla JavaScript.
The method of sharing data using a class that we are discussing is commonly referred to as a singleton pattern and it is a technique that has been around for a long time. The basic idea is that we want to share a single instance of a class among multiple pages/components/services, such that they are all accessing the same methods and data.
A class is like a blueprint for creating objects, and we can create multiple objects from a single class. For example, we can do this:
let myService = new MyService();
To create a new object based on the MyService
class, and we can do this as many times as we like. Each time we do it, we are creating a completely separate and independent object. With the singleton pattern, we would just do this once and create a single object based on the class that is shared throughout our entire application. In this way, that shared singleton object provides the same methods and data no matter what is accessing it or where it is accessing it from.
Creating a Singleton Service in StencilJS
The concept is simple enough: instantiate a single object from a class and share that object throughout the application. Let’s take a look at what that might actually look like though. We will create a simple dummy service to demonstrate.
Create a file at src/services/my-service.ts and add the following:
import { MyDataType } from "../interfaces/my-data";
class MyServiceController {
public myData: MyDataType[];
constructor() {}
async load() {
if (this.myData) {
return this.myData;
} else {
// Load data and then...
return this.myData;
}
}
async getData() {
const data = await this.load();
return data;
}
addData(data) {
this.myData.push(data);
}
}
export const MyService = new MyServiceController();
At this point, if you are familiar with Angular, you will probably that this looks almost identical to what you would do in Angular - except that there is no @Injectable
decorator, and we are exporting an instance of the class at the bottom.
We are just using some dummy methods and data here, but the general idea is that we have a service that is keeping track of some data (maybe posts
or todos
). We have the myData
class member that will store the data, we have a method to load
an initial set of data, we have a addData
method for adding more data, and a getData
method to return the current set of data.
We might want to make use of the methods/data available in this class in multiple pages/components in our StencilJS application. Perhaps from one page, we will want to call the getData
method to get all of the current data, but we might also want to call the addData
method from a different page and have that data made available to the page making the getData
call.
To achieve this, we create a new instance of the service like this:
export const MyService = new MyServiceController();
We not only create a new instance of MyServiceController
, we also export
it so that we will be able to import
this object elsewhere in our application. Notice that we declare the object using const
instead of say var
or let
. By using const
we ensure that our singleton service can’t be overwritten, which is the point of a singleton service - we want to declare a single instance and use that throughout the life of the application. It’s not required to use the const
keyword, but it does help enforce the “singleton-ness” of the object.
Now that we have our service, we just need to be able to make use of it throughout the application. To do that, you will be able to import
it anywhere like this:
import { MyService } from '../../services/my-service';
and then in the component that you have imported MyService
into, you will immediately be able to make use of all of the data/methods that the service provides:
MyService.addData(data);
this.data = await MyService.getData();
We will be able to import this service in any of our pages/component/services and we will be sharing the same instance. If we addData
in one component, and then later use getData
from a different component, the data added by one component will be available to the other.
Summary
This approach isn’t perhaps as robust or scalable as using a full state management solution like Redux, but it is a lot simpler and often an application will not require anything more advanced than a simple singleton to manage state. Although this kind of functionality isn’t available out of the box like it is with a full/opinionated framework like Angular, it is quite simple to achieve with just standard ES6 JavaScript classes.