Exploring state management in an old project, I found that my current system utilizes MobX. Transitioning to server-side rendering (SSR) seemed straightforward in newer projects but proved troublesome with TypeScript.
The approach involved a centralized store manager overseeing all stores, allowing them to access and manipulate each other's data in JavaScript. Unfortunately, TypeScript presented challenges.
To pinpoint the issue, I created a reproducible example. Feel free to test it in the TypeScript playground to fully grasp the problem:
/**
* The StoreManager oversees all application stores
*/
class StoreManager<T extends Record<string, InitializableStore>> {
public stores: T = {} as any;
constructor(public instantiators: { [K in keyof T]: (manager: any) => T[K] }) {
for (const [name, creator] of Object.entries(instantiators)) {
this.stores[name as keyof T] = creator(this);
}
}
public async init() {
console.info("Initializing stores");
await Promise.all(Object.values(this.stores).map((x) => x.init()));
}
}
export type Manager = StoreManager<Stores>;
/**
* Represents a store with access to the manager
*/
class InitializableStore {
constructor(protected manager: Manager) {}
public init(): void | Promise<void> {}
}
/**
* Creates a store factory helper function
*/
const createStoreFactory = <S extends InitializableStore>(
storeClass: new (manager: Manager) => S,
) => (manager: Manager) => new storeClass(manager);
/**
* Setting up example stores
*/
class StoreA extends InitializableStore {
public init() {}
public meow() {
console.log("Meow");
}
}
class StoreB extends InitializableStore {
public init() {
const { storeA } = this.manager.stores;
storeA.meow();
}
public woof() {
console.log("Woof!");
}
}
const storeA = createStoreFactory(StoreA);
const storeB = createStoreFactory(StoreB);
/**
* Defining the stores for the manager here
* */
const stores = { storeA, storeB };
export type StoreMapReturn<
T extends Record<string, (manager: Manager) => InitializableStore>
> = {
[K in keyof T]: ReturnType<T[K]>;
};
/**
* This results in an error due to a circular reference
*/
export type Stores = StoreMapReturn<typeof stores;
The complexity arises from the need for stores to interact with the manager while avoiding circular references. Ideally, the setup should:
- Allow easy manager access from any store
- Avoid relying on a global object imported externally for the manager, making it encapsulated and flexible
- Ensure type safety when accessing stores through the manager