Reactive Store
Utility
This library allows us to update domain objects from our Application Layer by implementing interfaces of our Domain Services.
Installation
You can install this store as follows:
npm i --save @codescouts/store
Dependencies
Updating our UI from an application layer
We’ll start with the example provided by the template we’ve built 👇
npx create-react-app my-app --template @codescouts/clean-architecture-template
Using this example, we have a use case in the Application Layer that adds a log, and subsequently, we need to update the UI so the user can see the logs being added, right?
When we invoke the execute method of the use case, we’ll be able to utilise domain objects, but occasionally, we’ll also want to update the UI.
If we inject a DomainService using our library, we can not only access the objects stored within but also update the UI whenever they are modified.
Our use case
import { IEventDispatcher } from "@codescouts/events";
import { Log } from "@domain/model/Log";
import { LoggerService } from "@domain/services/LoggerService";
export class TestUseCase {
constructor(private readonly loggerService: LoggerService) {}
public execute(message: string) {
const log = new Log(message);
this.loggerService.update(log);
}
}
Our Domain Service
import { Log } from "../model/Log";
export interface LoggerService {
logs: Log[];
save: (log: Log) => void;
}
Our React Component
Let’s look at the following UI component that displays the logs.
import styles from "./Logs.module.css";
import { useHomeViewModel } from "../useHomeViewModel";
export const Logs = () => {
const { logs, test, input } = useHomeViewModel();
return (
<div className={styles.log}>
<div className={styles.logContainer}>
<input
className={styles.text}
type="text"
ref={input}
placeholder="Write log"
onKeyDown={(e) => {
if (e.key === "Enter") {
test();
}
}}
/>
<button className={styles.addLog} onClick={test}>
Add Log
</button>
</div>
<ul>
{logs.map((log) => (
<li key={log.when}>{log.format()}</li>
))}
</ul>
</div>
);
};
As you can see on line 27, we are using the instance of the Log class. Let’s look at its implementation:
export class Log {
public readonly when: number;
constructor(public readonly message: string) {
this.when = new Date().getTime();
}
public format() {
return `${this.message} - ${new Date(this.when).toDateString()}`;
}
}
Referring back to the architecture outlined here Clean architecture, we can see we use a ViewModel as a component to decouple the UI from its behaviour. It looks something like this 👇
import { useCallback, useRef } from "react";
import { TestUseCase } from "@application/test-use-case";
import { useLogger } from "@infrastructure/services";
export const useHomeViewModel = () => {
const input = useRef<HTMLInputElement>(null);
const loggerService = useLogger();
const testUseCase = new TestUseCase(loggerService);
const test = useCallback(() => {
if (!input.current!.value) return;
testUseCase.execute(input.current!.value);
}, [testUseCase]);
return { logs, test, input };
};
Now, how would we implement useLogger as an implementation of our DomainService? Let’s take a look 👇
Domain Service Implementation
import { create } from "@codescouts/store";
import { Log } from "@domain/model";
import { LoggerService } from "@domain/services";
export const useLogger = create<LoggerService>((set) => ({
logs: [],
save: (log: Log) => set((state) => ({ logs: [...state.logs, log] })),
}))
.withPersist(Log)
.build();
As shown here, this store takes the object you want to implement in its Generic, and when constructing it, it receives a setter. This setter allows you to update the object.
Persisting the store
Our library supports saving and restoring domain objects even when the user refreshes the browser.
To do this, you must use the withPersist method and pass your object reference as an argument.
If you wish to save your objects in localstorage to restore them later, invoke the withPersist function and reference your class as an argument.
export const useLogger = create<LoggerService>((set) => ({
logs: [],
save: (log: Log) => set((state) => ({ logs: [...state.logs, log] })),
}))
.withPersist(Log)
.build();
Our store will reinstantiate the class and all its relationships, so you can fully utilise its behaviour again.
Restoring state
class Foo {
inner: Bar;
public constructor() {
this.inner = new Bar();
}
foo() {}
}
Our store can not only restore the instance of the Foo class but will also restore the instance of the Bar class.
Finally, you need to execute the build method to create the state according to your configuration.
This store is also available for Svelte, maintaining the same API.
npm i --save @codescouts/svelte-store
Instantiating the use case with Dependency Injection
You can find the documentation here: How to inject dependencies?
Once you configure the dependency injector, it will look like this:
import { useCallback, useRef } from "react";
import { useResolve } from "@codescouts/ui";
import { TestUseCase } from "@application/test-use-case";
import { useLogger } from "@infrastructure/services";
export const useHomeViewModel = () => {
const input = useRef<HTMLInputElement>(null);
const { logs } = useLogger();
const testUseCase = useResolve(TestUseCase);
const test = useCallback(() => {
if (!input.current!.value) return;
testUseCase.execute(input.current!.value);
input.current!.value = "";
input.current!.focus();
}, [testUseCase]);
return { logs, test, input };
};