Skip to main content
Version: v2.x.x

Services

A service contain methods, it can be simple methods or event hooks

type State = { count: number };

export class CounterService extends Service<State> {
state: State = { count: 0 };

async decrement() {
if (this.state.count !== 0) {
this.state.count--;
}
}
}

The methods of a service can:

Access to the state

You can access the local state by reading and writing directly on its properties. Thanks to valtio, the state is automatically reactive.

In this example the counter is decremented only if it is not 0

type State = { count: number };

export class CounterService extends Service<State> {
state: State = { count: 0 };

async decrement() {
if (this.state.count !== 0) {
this.state.count--;
}
}
}

Access to the other services

For exemple we have a todoList form, it should append the form values to a todoList, then the form resets.

The submit method of TodoFormService calls the method append of TodoListService

type Form = { name: string };

type TodoFormState = Form;
export class TodoFormService extends Service<TodoFormState> {
state: TodoFormState = { name: "" };

submit() {
const todoListService = this.getService("todoList");
todoListService.append(this.state);

this.state.name = "";
}
}

This way each service can have a single responsibility

init()

You can write an init method that will be executed right after the core is instantiated It is the perfect place to initialize listeners or setup dependencies

type State = { messages: string[] };

export class MessageService extends Service<State> {
state: State = { messages: [] };

init() {
this.dependencies.notifications.onReceive(this.onReceive);
}

onReceive = (receivedNotification: string) => {
this.state.messages.push(receivedNotification);
}
}

Subscribe to state changes

Use this.subscribe() to listen to state changes within a service:

export class UserService extends Service<{ name: string }> {
state = { name: "" };

init() {
this.subscribe((state) => {
console.log("User name changed:", state.name);
});
}

setName(name: string) {
this.state.name = name; // Will trigger the subscription
}
}

The dependencies

If your app is in clean architecture and uses the port-adapter pattern, you can access any of the dependencies in any service method

interface ShoesApiGateway {
get(): Promise<Shoe[]>;
}

type Dependencies = {
shoesApi: ShoesApiGateway;
};
type State = Shoe[];

export class ShoesService extends Service<State> {
state: State = [];

async loadShoes() {
const shoes = await this.dependencies.shoesApi.get();
this.state.push(...shoes);
}
}

The dependencies can then be injected when creating the core

class RealShoesApiAdapter implements ShoesApiGateway {
async get() {
return axios.get<Shoe[]>("https://my-api/shoes/get");
}
}

const core = createCore({ shoesApi: new RealShoesApiAdapter() });