Views: 1408
Number of votes: 0
Average rating:

Frontend: Adding application services - Into Foundation Spa React series, Part 6

In this sixth - and final - installment of the  "Into Foundation-Spa-React" series, the focus is on the service container bundled within the Foundation Spa React and how it might be leveraged when building your own solution based upon the core libraries that power Foundation Spa React.

First is the use-case. I most cases you wouldn't need to add your own services, as you can leverage the context/provider and hooks from React to make a service available. The case where this will be most useful is where you need to replace one of the standard services provided by the framework, need to interact with the bootstrapping process, and/or have supporting services that you need to be able to reference when needed.

The most used services from the service container are the Content Repository and Content Delivery Client. The first provides a proxy to the second, adding (when running in-browser and non-edit) caching of content items using IndexDB. The second is used directly for "non-content" operations, such as authentication, controller methods.

In order to interact with the bootstrapping process, you'll need to provide an instance of Core.IInitializableModule, which can easily be created by extending Core.BaseInitializableModule. During the bootstrapping process, the system invokes the three methods in the following order:

  • After the core services have been registered, but before any initialization has taken place: ConfigureContainer; This is where you should add/configure services within the container.
  • After the container has been created, when the initialization creates the Redux state container: GetStateReducer; This is where you can add your reducers for the global state container
  • As the last step of the bootstrapping process: StartModule; here you can execute any logic needed to bootstrap your own logic.

At this time all three methods run synchronously, which could cause a long execution time on the main thread if not used sparsely and critically.

As you might have noticed, the Core.IInitializableModule has a SortOrder, which defines the execution sequence of the modules. They are executed in ascending order, the system guarantees that all modules with a lower SortOrder have been executed successfully, however, modules with the same SortOrder can be executed in parallel, before or after the current module. Modules with a higher SortOrder will always be executed after this module.

In action

So, a very long explanation, let's see how this works in practice, by looking at one of the core modules within the SPA: "Core State Engine"

export class StateModule extends BaseInitializableModule implements IInitializableModule
{
    protected name = "Core State Engine";
 
    public SortOrder = 40;
 
    public StartModule(context: IEpiserverContext): void
    {
        const store = context.getStore();
        const state = store.getState() as PartialAppState;
        const cfg = context.serviceContainer.getService<Readonly<IConfig>>(DefaultServices.Config);
 
        // Setup CD-API Language to respond to the state changes, ensuring
        // that it always takes the current CD-API instance from the container.
        Tools.observeStore<string, CmsAppState>(
            store,
            (x) => x?.OptiContentCloud?.currentLanguage || cfg.defaultLanguage,
            (newValue) => {
                if (newValue) {
                    const cdAPI = context.serviceContainer.getService<IContentDeliveryAPI>(DefaultServices.ContentDeliveryAPI_V2);
                    cdAPI.Language = newValue;
                }
            }
        );
 
        // Make sure the current language is applied
        const language = state?.OptiContentCloud?.currentLanguage;
        if (!language) {
            store.dispatch({
                type: "OptiContentCloud/SetState",
                currentLanguage: cfg.defaultLanguage
            })
        } else {
            const cdAPI = context.serviceContainer.getService<IContentDeliveryAPI>(DefaultServices.ContentDeliveryAPI_V2);
            cdAPI.Language = language || cfg.defaultLanguage;
        }
 
    }
 
    /**
     * Return the standard state reducer for the CMS Status
     */
    public GetStateReducer : () => IStateReducerInfo<any> = () => CmsStateReducer;
}

So what happens in this module? First, we see that there's no override of the ConfigureContainer method; this is possible as we're extending BaseInitializableModule that provides basic empty methods for each of the three required methods from IInitializableModule. So this module does not affect the Container itself. Then in the second step, it registers the reducer for Redux that will manage the state, then finally, when the site starts it ensures that we have a language in the state and that it is synchronized with the current language setting of the ContentDeliveryAPI.

Ok, you've created your IInitializableModule, great, but JavaScript doesn't allow for automatic class discovery. So the last step is to register it within your configuration.

// Partial configuration shown, with just the items relevant to this article.
export const config : SpaConfig = {
    // Initialization modules
    modules: [
        new CommerceInitialization()
    ]
}

This being JavaScript, the instantiation is only required because the module extends BaseInitializableModule, it is possible to create a static object that implements the interface as well, however that would mean that you need to implement the full interface.

But wait, the title said it was possible to add application services? Yes, it is possible, as shown in the Routing Module. 

export default class RoutingModule extends BaseInitializableModule implements IInitializableModule
{
    protected name = "Optimizely CMS Routing";
    public readonly SortOrder = 20;
   
    /**
     * Ensure the configuration object within the service container contains a "*" route. If
     * this "*" route is not claimed by the implementation, it will be added as fall-back to
     * Episerver CMS based routing.
     *
     * @param {IServiceContainer} container The Service Container to update
     */
    public ConfigureContainer(container: IServiceContainer) : void
    {
        const config = container.getService<AppConfig>(DefaultServices.Config);
        let haveStar = false;
        config.routes = config.routes || [];
        config.routes.forEach(c => haveStar = haveStar || c.path === "*");
        if (!haveStar) config.routes.push({
            path: "*",
            component: RoutedComponent
        });
    }
}

So instead of getting a service from the container (in this case Configuration), it is also possible to set/add services into the container. The DefaultServices here is just a list of predefined strings, so for your own services, use whatever name you like. A good convention would be [Module].[Service], for example, "Routing.Router";

 And that's a wrap. Feel free to star the project on GitHub, leave feedback here or on GitHub, use it to kick-start your own Optimizely powered SPA. Anyway, I hope this series gave a little insight into the inner workings of Foundation-Spa-React.

Aug 02, 2021

Please login to comment.