Anton Shuvalov

Generic Service Locator Pattern

Pattern Description

Service locator acts as a singleton registry for all services used by an application. This pattern is quite old and it was mentioned for the first time in Martin Fawler's blog in 2004 year. The pattern has some fair critic, so it's quite reasonable to read it in advance.

Below there is an implementation of this pattern via Mobx and NextJS.

Further Reading:


Implementation

export class ServiceLocator<T, K extends keyof T = keyof T> {
  private _services: Record<K, T[K]> = {} as T;

  public add(serviceId: K, service: any) {
    this._services[serviceId] = service;
  }

  public get<L extends keyof T>(id: L): T[L] {
    return (this._services as any)[id];
  }
}

Setup

interface ServiceMap {
  authStore: AuthStore;
  postsStore: PostsStore;
  commentsStore: CommentsStore;

  authAPI: AuthAPI;
  postsAPI: PostsAPI;
  commentsAPI: CommentsAPI;
}

export const serviceLocator = new ServiceLocator<ServiceMap>();

export class AppStore {
  constructor() {
    serviceLocator.add('authStore', new AuthStore());
    serviceLocator.add('postsStore', new PostsStore());
    serviceLocator.add('commentsStore', new CommentsStore());

    serviceLocator.add('authAPI', new AuthAPI());
    serviceLocator.add('postsAPI', new PostsAPI());
    serviceLocator.add('commentsAPI', new CommentsAPI());
  }
}

React Context

// app/store/AppStoreProvider
import React, { FC, useState } from 'react';
import { AppStore, serviceLocator, ServiceMap } from 'app/AppStore';

const AppStoreContext = React.createContext<AppStore | null>(null);

export const AppStoreProvider: FC = ({ children }) => {
  const [appStore] = useState(() => new AppStore());
  return <AppStoreContext.Provider value={appStore}>{children}</AppStoreContext.Provider>;
};

export const useStore = <T extends keyof ServiceMap>(storeId: T) => serviceLocator.get(storeId);
// pages/_app.tsx
import React, { FC } from 'react';
import type { AppProps } from 'next/app';
import { AppStoreProvider } from 'app/store/AppStoreProvider';

const MyApp: FC<AppProps> = ({ Component, pageProps }) => {
  return (
    <AppStoreProvider>
      <Component {...pageProps} />
    </AppStoreProvider>
  )
}

Usage in Services

class CommentsStore {
  public async create(msg: string) {
    const commentsAPI = serviceLocator.get('commentsAPI');
    await commentsAPI.postComment(message);
  }
}

Usage in React Components

import React, { FC } from 'react';
import { observer } from 'mobx-react-lite';
import { useStore } from 'app/AppStoreContext';

export const PostsList: FC = observer(() => {
  const postsStore = useStore('postsStore');
  return (
    <div className={s.list}>
      {postsStore.map(post => (
        <PostSnippet post={post} />
      ))}
    </div>
  );
});

Queueing Requests

Instead of taking instances of the services, the service locator can take fabrics, and implement the get method as an async function. This way may help to handle much more complex initialization logic when some services request the ones that have not been initialized yet. But with great power comes great responsibility, be careful with this.


Created with obsidian-blog