ReactJS + MobX - experience using DI

It seems to me that the time has come to share the approach for writing the ReactJS App, I do not pretend to be unique.

You can skip the first paragraph . I’ve been working on web development for a long time, but for the last four years I've been sitting tight at ReactJS and I’m happy with everything, redux in my life, but about two years ago I met MobX, just a couple of months ago I tried to return to redux, but I don’t I could, I had a feeling that I was doing something extra, maybe something was wrong, many bytes on servers were already translated on this topic, the article is not about the coolness of one before the other, this is just an attempt to share its work, maybe someone really This approach will go down to the point.

Tasks that we will solve:

  • connection di for components
  • server rendering with asynchronous data loading

The structure of the project can be viewed at Github . Therefore, I will skip how to write a primitive application and the article will contain only the main points.

We introduce such concepts as: data model, service, stor.

Let's get a simple model

TodoModel.ts
import { observable, action } from'mobx';
exportclassTodoModel{
  @observable public id: number;
  @observable public text: string = '';
  @observable public isCompleted: boolean = false;
  @action
  public set = (key: 'text' | 'isCompleted', value: any): void => {
    this[key] = value;
  };
}


what you see as a set action, in a model is more an exception than a good tone, usually there is a basic model with primitive helpers in the project and from it I simply inherit, in models there should not be action games at all.

Now we need to learn how to work with this model, we will get the service:

TodoService.ts
import { Service, Inject } from'typedi';
import { plainToClass, classToClass } from'class-transformer';
import { DataStorage } from'../storage/DataStorage';
import { action } from'mobx';
import { TodoModel } from'../models/TodoModel';
const responseMock = {
  items: [
    {
      id: 1,
      isCompleted: false,
      text: 'Item 1'
    },
    {
      id: 2,
      isCompleted: true,
      text: 'Item 2'
    }
  ]
};
@Service('TodoService')
exportclassTodoService{
  @Inject('DataStorage')
  public dataStorage: DataStorage;
  @action
  public load = async () => {
    awaitnewPromise(resolve => setTimeout(resolve, 300));
    this.dataStorage.todos = plainToClass(TodoModel, responseMock.items);
  };
  @action
  public save(todo: TodoModel): void {
    if (todo.id) {
      const idx = this.dataStorage.todos.findIndex(item => todo.id === item.id);
      this.dataStorage.todos[idx] = classToClass(todo);
    } else {
      const todos = this.dataStorage.todos.slice();
      todo.id = Math.floor(Math.random() * Math.floor(100000));
      todos.push(todo);
      this.dataStorage.todos = todos;
    }
    this.clearTodo();
  }
  @action
  public edit(todo: TodoModel): void {
    this.dataStorage.todo = classToClass(todo);
  }
  @action
  public clearTodo(): void {
    this.dataStorage.todo = new TodoModel();
  }
}


In our service there is a link to

DataStorage.ts
import { Service } from'typedi';
import { observable } from'mobx';
import { TodoModel } from'../models/TodoModel';
@Service('DataStorage')
exportclassDataStorage{
  @observable public todos: TodoModel[] = [];
  @observable public todo: TodoModel = new TodoModel();
}


In this store, we will store the state of our application, there may be many such stores, but as practice has shown, there is no point in breaking into many small stores. In the stores as well as in the models there should not be action games.

We have almost everything ready, all that is left is to connect to our application, for this we need a bit of injector from mobx-react:

DI
import { inject } from'mobx-react';
exportfunctionDI(...classNames: string[]) {
  return(target: any) => {
    return inject((props: any) => {
      const data: any = {};
      classNames.forEach(className => {
        const name = className.charAt(0).toLowerCase() + className.slice(1);
        data[name] = props.container.get(className);
      });
      data.container = props.container;
      return data;
    })(target);
  };
}


and get a container for our DI

browser.tsx
import'reflect-metadata';
import * as React from'react';
import { hydrate } from'react-dom';
import { renderRoutes } from'react-router-config';
import { Provider } from'mobx-react';
import { BrowserRouter } from'react-router-dom';
import { Container } from'typedi';
import'../application';
import { routes } from'../application/route';
hydrate(
  <Providercontainer={Container}><BrowserRouter>{renderRoutes(routes)}</BrowserRouter></Provider>,
  document.getElementById('root')
);


For the browser, we always have one container, but for the server render, you need to look, it is better to organize your container for each request:

server.tsx
import * as express from'express';
import * as React from'react';
import { Container } from'typedi';
import'../application';
// @ts-ignoreimport * as mustacheExpress from'mustache-express';
import * as path from'path';
import { renderToString } from'react-dom/server';
import { StaticRouter } from'react-router';
import { Provider } from'mobx-react';
import * as uuid from'uuid';
import { renderRoutes, matchRoutes } from'react-router-config';
import { routes } from'../application/route';
const app = express();
const ROOT_PATH = process.env.ROOT_PATH;
const currentPath = path.join(ROOT_PATH, 'dist', 'server');
const publicPath = path.join(ROOT_PATH, 'dist', 'public');
app.engine('html', mustacheExpress());
app.set('view engine', 'html');
app.set('views', currentPath + '/views');
app.use(express.static(publicPath));
app.get('/favicon.ico', (req, res) => res.status(500).end());
app.get('*', async (request, response) => {
  const context: any = {};
  const id = uuid.v4();
  const container = Container.of(id);
  const branch = matchRoutes(routes, request.url);
  const promises = branch.map(({ route, match }: any) => {
    return route.component && route.component.loadData ? route.component.loadData(container, match) : Promise.resolve(null);
  });
  awaitPromise.all(promises);
  const markup = renderToString(
    <Providercontainer={container}><StaticRouterlocation={request.url}context={context}>
        {renderRoutes(routes)}
      </StaticRouter></Provider>
  );
  Container.remove(id);
  if (context.url) {
    return response.redirect(
      context.location.pathname + context.location.search
    );
  }
  return response.render('index', { markup });
});
app.listen(2016, () => {
  // tslint:disable-next-lineconsole.info("application started at 2016 port");
});


The server render is actually a delicate thing, on the one hand, I want to let everything go through it, but it has only one business task, to give content to bots , so it’s better to check for something like this “has the user been authorized at least once , and skip server render with the creation of containers on the server.

Well, now to our components:

MainRoute.tsx
import * as React from'react';
import { TodoService } from'../service/TodoService';
import { observer } from'mobx-react';
import { DI } from'../annotation/DI';
import { DataStorage } from'../storage/DataStorage';
import { Todo } from'../component/todo';
import { Form } from'../component/form/Form';
import { ContainerInstance } from'typedi';
interface IProps {
  todoService?: TodoService;
  dataStorage?: DataStorage;
}
@DI('TodoService', 'DataStorage')
@observer
exportclassMainRouteextendsReact.Component<IProps> {
  public staticasync loadData(container: ContainerInstance) {
    const todoService: TodoService = container.get('TodoService');
    await todoService.load();
  }
  public componentDidMount() {
    this.props.todoService.load();
  }
  public render() {
    return (
      <div><Form /><ul>
          {this.props.dataStorage.items.map(item => (
            <likey={item.id} ><Todomodel={item} /></li>
          ))}
        </ul></div>
    );
  }
}


Here everything turns out very logical and beautiful, our “render” view for drawing takes data from our site, the component hooks say at what point in time we should load the data.

Todo.tsx
import * as React from'react';
import { TodoModel } from'../../models/TodoModel';
import { TodoService } from'../../service/TodoService';
import { DI } from'../../annotation/DI';
import { observer } from'mobx-react';
interface IProps {
  model: TodoModel;
  todoService?: TodoService;
}
@DI('TodoService')
@observer
exportclassTodoextendsReact.Component<IProps> {
  public render() {
    const { model, todoService } = this.props;
    return (
      <>
        <input
          type='checkbox'
          checked={model.isCompleted}
          onChange={e => model.set('isCompleted', e.target.checked)}
        />
        <h4>{model.text}</h4>
        <button type='button' onClick={() => todoService.edit(model)}>Edit</button>
      </>
    );
  }
}


Form.tsx
import * as React from'react';
import { observer } from'mobx-react';
import { DI } from'../../annotation/DI';
import { TodoService } from'../../service';
import { DataStorage } from'../../storage';
import { TextField } from'../text-field';
interface IProps {
  todoService?: TodoService;
  dataStorage?: DataStorage;
}
@DI('TodoService', 'DataStorage')
@observer
exportclassFormextendsReact.Component<IProps> {
  public handleSave = (e: any) => {
    e.preventDefault();
    this.props.todoService.save(this.props.dataStorage.todo);
  };
  public handleClear = () => {
    this.props.todoService.clearTodo();
  };
  public render() {
    const { dataStorage } = this.props;
    return (
      <formonSubmit={this.handleSave}><TextFieldname='text'model={dataStorage.todo} /><button>{dataStorage.todo.id ? 'Save' : 'Create'}</button><buttontype='button'onClick={this.handleClear}>
          Clear
        </button></form>
    );
  }
}


In my opinion, working with forms is much more convenient through the models / dtoshki, you can use the usual native forms, and updating the data model and everyone who listens to it will be updated instantly.

This is how I use this combination of libraries: react, class-transformer, mobx, typedi

We now use this approach in the sale, these are very large projects, with common common components and services.

If this approach is interesting, I will tell you how In the key, we do model validation before sending to the server, how we handle server errors, and how we synchronize our state between browser tabs.

In fact, everything is very bonal: “class-validator”, “localStorage + window.addEventListener ('storage')”

Thank you for reading :-)

Example

Only registered users can participate in the survey. Sign in , please.

Need a sequel?


Also popular now: