terso
v0.1.0
Published
Dependency injection hooks for React
Downloads
1
Readme
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
1.1 Configure typescript
Add these entries in the compilerOptions
of your typescript config
"experimentalDecorators": true,
"types": ["reflect-metadata"],
1.2 Copy/paste qwickstart
interface TestModel extends ViewModel {
value: string;
}
const value = "the value";
@injectable()
class TestPresenter implements Presenter<TestModel> {
loadViewModel(): Promise<void> {
return Promise.resolve();
}
cleanModel(): Promise<void> {
return Promise.resolve();
}
get viewModel() {
return {
value: value,
};
}
}
function App() {
const model = useModel<TestModel>(TestPresenter);
return <div>{model.value}</div>;
}
function setup(container: Container) {
container
.bind<TestPresenter>(TestPresenter)
.to(TestPresenter)
.inSingletonScope();
}
const WithIoc = withIoc(App, setup);
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(<WithIoc />);
2. Wrap your App
in the terso
IoC Context
// App.tsx
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);
// ioc/ioc.types.ts
export const TYPES = {
TodoStore: Symbol.for("TodoStore"),
TodoBaseUrl: Symbol.for("TodoBaseUrl"),
};
// oic.config.js
import "reflect-metadata"
import {TYPES} from "./ioc.types";
//import {TodoStore } from "../stores/TodoStore"
//import {TodoStoreImpl } from "../stores/impl/TodoStoreImpl"
export function configureContainer(container: Container) {
// define here the dependencies of your code: services, api, repositories, configurattions
// for example:
container.bind<string>(TYPES.TodoBaseUrl).toConstantValue(/*config.todoBaseUrl*/ "some url");
// container
// .bind<TodoStore>(TYPES.TodoStore)
// .to(TodoStoreImpl)
// .inSingletonScope();
}
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
useModel
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
// Todo.tsx
import { observer } from "mobx-react-lite";
import { Todo as TodoType } from "../../../domain/Todo";
import { useModel } from "terso";
import { TodoPresenter, TodoViewModel } from "../../../presenter/TodoPresenter";
interface TodoProps {
todo: TodoType;
}
export default observer(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>
);
})
// TodoPresenter.ts
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,
};
}
}