Terso
dependency injection hooks for React.
Terso is a pragmatic hook and HOC library for React, that provides dependency injection to a React application.
It uses inversify js as an IoC container, in combination with mobx for state management.
Is it a revolutionary library? No! It is just a collection of functions, born with the sole need of not wanting to copy/paste the same code into different projects.
Quick start
Quick start in three easy steps
1. Install
npm install terso
# yarn add terso
2. Wrap your App
in the terso
IoC Context
import { withIoc } from "terso";
import { Container } from "inversify";
import { configureContainer } from "./ioc/ioc.config";
function App() {
return <main>My app</main>
}
export default withIoc(App, configureContainer);
export const TYPES = {
TodoStore: Symbol.for("TodoStore"),
TodoBaseUrl: Symbol.for("TodoBaseUrl"),
};
import {TYPES} from "./ioc.types";
export function configureContainer(container: Container) {
container.bind<string>(TYPES.TodoBaseUrl).toConstantValue( "some url");
}
The function configureContainer
is mandatory and takes the IoC container as an argument. You can add (bind) alle the dependency you need in the app into the container.
3. Use a dependency in React Components
import { useInject } from "terso";
import { observer } from "mobx-react-lite";
import { TYPES } from "../ioc/ioc.types";
import { TodoStore } from "../stores/TodoStore";
export default observer(function Todolist() {
const todoStore = useInject<TodoStore>(TYPES.TodoStore);
return (
<main>
<h2>Todo list</h2>
{todoStore.todos
.slice()
.map((todo) => (
<Todo key={todo.id} todo={todo} />
))}
</main>
);
})
Hooks
terso
provides these hooks:
useInject
The hook useInject
takes an object from the IoC container and returns it, ready to be used in a React component.
Signature
useInject<T>(type: ServiceIdentifier<T>): T
Usage
const myService = useInject<MyServiceType>(MyServiceIdentifier);
Example
import { useInject } from "terso";
import { observer } from "mobx-react-lite";
import { TYPES } from "../ioc/ioc.types";
import { TodoStore } from "../stores/TodoStore";
export default observer(function Todolist() {
const todoStore = useInject<TodoStore>(TYPES.TodoStore);
return (
<main>
<h2>Todo list</h2>
{todoStore.todos
.slice()
.map((todo) => (
<Todo key={todo.id} todo={todo} />
))}
</main>
);
})
useModel
useModel
provides a ViewModel
implementation in a React Component.
A ViewModel
is an interface, borrowed from the famous pattern Model-View-Presenter. To use it you have to create an implementation of the ViewModel
interface and an implementation of a Presenter
that are defined as follows:
export interface ViewModel {
[key: string]: any;
}
export interface Presenter {
loadViewModel(): Promise<void>;
cleanModel(): Promise<void>;
viewModel: ViewModel;
}
The Presenter
interface provides a method to load the ViewModel
, a method to clean, and the ViewModel
itself.
Presenters are useful for leaving React components simple by giving them a flat object to display: the ViewModel
. The presenter hides the business logic from the React component, so finally React components can be used for what they were designed: creating user interfaces.
Signature
export function useModel<T extends ViewModel>(type: ServiceIdentifier<Presenter>): T
Usage
const viewModel = useModel<MyViewModelType>(MyPresenterIdentifier);
Example
import { Todo as TodoType } from "../../../domain/Todo";
import { useModel } from "terso";
import { TodoPresenter, TodoViewModel } from "../../../presenter/TodoPresenter";
interface TodoProps {
todo: TodoType;
}
export default function Todo({ todo }: TodoProps) {
const viewModel = useModel<TodoViewModel>(TodoPresenter);
return (
<li className="todo-card">
<span className={todo.completed ? "done" : "todo"}>
{todo.id} - {todo.title}
</span>
{viewModel.canDelete && <button>delete</button>}
</li>
);
}
import { inject, injectable } from "inversify";
import { action, makeObservable, observable } from "mobx";
import { TYPES } from "../ioc/ioc.types";
import {
Permissions,
type AuthorizationService,
} from "../service/AuthorizationService";
import { type TodoStore } from "../service/TodoService";
import { Presenter, ViewModel } from "terso";
export interface TodoViewModel extends ViewModel {
canDelete: boolean;
}
@injectable()
export class TodoPresenter implements Presenter {
@inject(TYPES.TodoStore)
private readonly todoService!: TodoStore;
@inject(TYPES.AuthorizationServiceType)
private readonly authService!: AuthorizationService;
private canDelete: boolean = false;
constructor() {
makeObservable<TodoPresenter, "canDelete">(this, {
canDelete: observable,
loadViewModel: action,
});
}
loadViewModel(): Promise<void> {
this.canDelete = this.authService.hasPermission(Permissions.todo.delete);
return Promise.resolve();
}
cleanModel(): Promise<void> {
return Promise.resolve();
}
get viewModel(): TodoViewModel {
return {
canDelete: this.canDelete,
};
}
}