Adding a service
This step consists in creating its own service.
The topic on this page is for advanced use cases.
You will implement a service when you need to add advanced logic to your application
Almost all the functionality provided by onekijs
are based on services.
The role of a service is to mutate a state. This state can be
- a component state. In this case, the service is called a "local service"
Example of hooks exposing a local service:useGet
,usePost
, ... - the global redux state. In this case, the service is called a global service.
Example of hooks exposing global services:useNotificationService
,useSecurityContext
The service mutates the state following best practices (immutability, action, reducers, ...)
To mutate the state, the service exposes two types of methods:
Reducer methods
(annoted with @reducer). Only these methods can mutate the state.Saga methods
(annotate with @saga). These methods can orchestrate calls to reducer methods and are used to handle complex scenarios such as:- Calling a reducer to update a "loading" variable to
true
- Letting React render the component to display the load indicator
- Making an AJAX request asynchronously
- Storing the result in the state (by calling another reducer method)
- Calling a reducer to update the "loading" variable to
false
- Calling a reducer to update a "loading" variable to
Final result
The result of this step is as follows:
The product detail page now displays a new "Check Availability" button
When you click on this button, the application will call the server to check if the product is available or not
- Vite App
- Nextjs App
Creating a service
We want to add a button on the product page to check its availability. Clicking on the button triggers the following scenario:
- Add a loading indicator on the button
- Disable the button
- Make a request to the server to check availability
- Display the result on the product page next to the button:
- Remove the loading indicator
- Activate the button
The state therefore contains the following elements:
export interface AvailabilityState {
// a flag to indicate if a request is progress
loading: boolean;
// the result of the request
// initial value is undefined
available?: boolean; // number of products available
}
Let's create a service to handle this state.
A service is a class that extends LocalService
or GlobalService
. In our case, the state is local to a component and so the class extends LocalService
The class must be annotated with @service. This is something specific to Typescript that allows onekijs
to create a proxy of this class at runtime.
@service
export default class AvailabilityService extends LocalService<AvailabilityState> {
...
}
Adding reducers
As mentioned before, the service contains two types of methods: reducer
and saga
For our service, we need two reducers:
- A reducer method to mutate the
loading
field. - A reducer method to mutate the
available
field.
Reducer methods must be annotated with @reducer
Let's add these two methods:
@service
export default class AvailabilityService extends LocalService<AvailabilityState> {
@reducer
setLoading(loading: boolean): void {
// the state is immutable
// however, the @reducer annotation allows updating the state
// like any other object (thanks to immer)
this.state.loading = loading;
}
@reducer
setData(data: AvailabilityResponse): void {
this.state.available = data.available;
this.setLoading(false); // A reducer can call another reducer
}
}
Adding sagas
For our service, we need a saga that will implement the following scenario:
- Update the field
loading
totrue
- Make a request to the server to check availability
- Put the result of the query in the state
- Update the field
loading
tofalse
Saga methods must be annotated with @saga
Let's add this method:
@service
export default class AvailabilityService extends LocalService<AvailabilityState> {
@reducer
setLoading(loading: boolean): void {
// the state is immutable
// however, the @reducer annotation allows updating the state
// like any other object (thanks to immer)
this.state.loading = loading;
}
@reducer
setData(data: AvailabilityResponse): void {
this.state.available = data.available;
this.setLoading(false); // A reducer can call another reducer
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@saga(SagaEffect.Latest)
*checkAvailability(productId: number) {
yield this.setLoading(true);
const data: AvailabilityResponse = yield asyncGet(`/products/${productId}/availability`);
yield this.setData(data);
}
}
Using the service
To instantiate this service, onekijs
provides the useLocalService
and useGlobalService
hooks
since we have created a local service, we must use useLocalService
:
Let's update the ProductDetails
component to add a Check Availability
button that uses the service:
- Vite App
- Nextjs App
loading...
loading...
Handling error
We can improve the service to properly handle errors returned by the server.
A common approch is to send a notification on the error
topic so the <NotificationCenter />
automatically displays it
To be able to use the NotificationService inside the AvailabilityService, one must inject it.
- Vite App
- Nextjs App
loading...
loading...
Next step
In the next step, we present you how to have a coherent look and feel thanks to **`onekijs-ui``.