Как стать автором
Обновить

Typescript и react

Время на прочтение7 мин
Количество просмотров24K

Разработка на javascript иногда становится похожа на работу детектива. Как понять чужой код? Хорошо, если разработчик обладает тонким искусством называть переменные так, чтобы другие поняли суть. А как быть, если члены команды все таки не всегда способны понять замысел своего коллеги? Как понять, что приходит в аргумент какой-либо функции?


Предположим, что аргумент функции называется errors. Вероятно в errors находится массив. Скорее всего строк? Ну то, что массив это понятно. Ведь далее проверяется его длина. Но свойство length есть и у строки. Похоже, чтобы точно разобраться, необходимо поставить breakpoint и запустить скрипт. Затем полностью пройти по сценарию на UI (например нам нужен финальный шаг формы). Теперь в devtools видно, что errors — это объект с набором определенных полей, среди которых и поле length.


Подобная неоднозначность при разборе javascript кода приводит к пустой трате времени разработчика. Неплохим решением в данном случае может стать typescript (далее ts). Можно использовать его в следующем проекте, а еще лучше сделать поддержку ts в существующем. После этого время на понимание чужого кода сократится значительно. Ведь, чтобы понять структуру любых данных достаточно одного клика. Можно сконцентрироваться на логике работы с данными и в любой момент времени знать, что вы однозначно понимаете работу кода.


Следует отметить некоторые достоинства ts. Его широко используют в различных фреймворках и он тесно связан с javascript. Развитие ts обусловливается потребностями frontend разработчиков.


В данной статье представлена разработка todo приложения, но только краткое описание интересных моментов. Полный код можно найти тут.


Я использовал react, typescript и mobx. Mobx — гибкое средство для управления состоянием приложения. Mobx лаконичен. Он позволяет работать с состоянием компонентов react в синхронном стиле. Нет проблем типа:


this.setState({name: 'another string'});
alert(this.state.name);

В данном случае выведется старое значение state.name.


Кроме того, mobx удобен и не мешает работать с типами ts. Можно описывать state в виде отдельных классов или прямо внутри react компонента.


Для простоты все компоненты помещены в папку components. В папке компонента определен класс с описанием состояния, связанного логически с отображением и работой с компонента.


В папке TodoItem находится файл с react компонентом TodoItem.tsx, файл со стилями TodoItem.module.scss и файл состояния TodoItemState.ts.


В TodoItemState.ts описаны поля для хранения данных, способы доступа к ним и правила их изменения. Круг возможностей очень велик благодаря ООП и ts. Часть данных может быть приватной, часть открыта только для чтения и прочее. С помощью декоратора @o указаны observable поля. На их изменения реагируют react компоненты. Декораторы @a (action) используются в методах для изменения состояния.


// TodoItemState.ts
import { action as a, observable as o } from 'mobx';

export interface ITodoItem {
  id: string;
  name: string;
  completed: boolean;
}

export class TodoItemState {
  @o public readonly value: ITodoItem;
  @o public isEditMode: boolean = false;

  constructor(value: ITodoItem) {
    this.value = value;
  }

  @a public setIsEditMode = (value: boolean = true) => {
    this.isEditMode = value;
  };
  @a public editName = (name: string) => {
    this.value.name = name;
  };
  @a public editCompleted = (completed: boolean) => {
    this.value.completed = completed;
  };
}

В TodoItem.tsx в props передается всего два свойства. В mobx оптимально для общей производительности приложения передавать сложные структуры данных в props react компонента. Поскольку мы используем ts, то можно точно указать тип принимаемого компонентом объекта.


// TodoItem.tsx
import React, { ChangeEventHandler } from 'react';
import { observer } from 'mobx-react';
import { TodoItemState } from './TodoItemState';
import { EditModal } from 'components/EditModal';
import classNames from 'classnames';
import classes from './TodoItem.module.scss';

export interface ITodoItemProps {
  todo: TodoItemState;
  onDelete: (id: string) => void;
}

@observer
export class TodoItem extends React.Component<ITodoItemProps> {
  private handleCompletedChange: ChangeEventHandler<HTMLInputElement> = e => {
    const {
      todo: { editCompleted },
    } = this.props;
    editCompleted(e.target.checked);
  };

  private handleDelete = () => {
    const { onDelete, todo } = this.props;
    onDelete(todo.value.id);
  };

  private get editModal() {
    const { todo } = this.props;
    if (!todo.isEditMode) return null;
    return (
      <EditModal
        name={todo.value.name}
        onSubmit={this.handleSubmitEditName}
        onClose={this.closeEditModal}
      />
    );
  }

  private handleSubmitEditName = (name: string) => {
    const { todo } = this.props;
    todo.editName(name);
    this.closeEditModal();
  };

  private closeEditModal = () => {
    const { todo } = this.props;
    todo.setIsEditMode(false);
  };
  private openEditModal = () => {
    const { todo } = this.props;
    todo.setIsEditMode();
  };

  render() {
    const { todo } = this.props;
    const { name, completed } = todo.value;
    return (
      <div className={classes.root}>
        <input
          className={classes.chackbox}
          type="checkbox"
          checked={completed}
          onChange={this.handleCompletedChange}
        />
        <div
          onClick={this.openEditModal}
          className={classNames(
            classes.name,
            completed && classes.completedName
          )}>
          {name}
        </div>
        <button onClick={this.handleDelete}>del</button>
        {this.editModal}
      </div>
    );
  }
}

В интерфейсе ITodoItemProps описано todo свойство типа TodoItemState. Таким образом внутри react компонента мы обеспечены данными для отображения и методами их изменения. Причем, ограничения на изменение данных можно описать как в state классе, так и в методах react компонента, в зависимости от поставленных задач.


Компонент TodoList похож на TodoItem. В TodoListState.ts можно заметить геттеры с декоратором @c (@computed). Это обычные геттеры классов, только их значения мемоизируются и пересчитываются при изменении их зависимостей. Computed по назначению похож на селекторы в redux. Удобно, что не нужно, подобно React.memo или reselect, явно передавать список зависимостей. React компоненты реагируют на изменение computed также как и на изменение observable. Интересной особенностью является то, что перерасчет значения не происходит, если в данный момент computed не участвует в рендере (что экономит ресурсы). Поэтому, несмотря на сохранение постоянных значений зависимостей, computed может пересчитаться (существует способ явно указать mobx, что необходимо сохранять значение computed).


// TodoListState.ts
import { action as a, observable as o, computed as c } from 'mobx';
import { ITodoItem, TodoItemState } from 'components/TodoItem';

export enum TCurrentView {
  completed,
  active,
  all,
}

export class TodoListState {
  @o public currentView: TCurrentView = TCurrentView.all;
  @o private _todos: TodoItemState[] = [];

  @c
  public get todos(): TodoItemState[] {
    switch (this.currentView) {
      case TCurrentView.active:
        return this.activeTodos;
      case TCurrentView.completed:
        return this.completedTodos;
      default:
        return this._todos;
    }
  }

  @c
  public get completedTodos() {
    return this._todos.filter(t => t.value.completed);
  }
  @c
  public get activeTodos() {
    return this._todos.filter(t => !t.value.completed);
  }

  @a public setTodos(todos: ITodoItem[]) {
    this._todos = todos.map(t => new TodoItemState(t));
  }

  @a
  public addTodo = (todo: ITodoItem) => {
    this._todos.push(new TodoItemState(todo));
  };
  @a
  public removeTodo = (id: string): boolean => {
    const index = this._todos.findIndex(todo => todo.value.id === id);
    if (index === -1) return false;
    this._todos.splice(index, 1);
    return true;
  };
}

Доступ к списку todo открыт только через computed поле, где, в зависимости от режима просмотра, возвращается необходимый отфильтрованный набор данных (завершенные, активные или все todo). В зависимостях todo указаны computed поля completedTodos, activeTodos и приватное observable поле _todos.


Рассмотрим главный компонент App. В нем рендерятся форма для добавления новых todo и список todo. Тут же создается экземпляр главного стейта AppSate.


// App.tsx
import React from 'react';
import { observer } from 'mobx-react';
import { TodoList, initialTodos } from 'components/TodoList';
import { AddTodo } from 'components/AddTodo';
import { AppState } from './AppState';
import classes from './App.module.scss';

export interface IAppProps {}

@observer
export class App extends React.Component<IAppProps> {
  private appState = new AppState();

  constructor(props: IAppProps) {
    super(props);
    this.appState.todoList.setTodos(initialTodos);
  }

  render() {
    const { addTodo, todoList } = this.appState;
    return (
      <div className={classes.root}>
        <div className={classes.container}>
          <AddTodo onAdd={addTodo} />
          <TodoList todoListState={todoList} />
        </div>
      </div>
    );
  }
}

В поле appState находится экземпляр класса TodoListState для отображения компонента TodoList и метод добавления новых todo, который передается в компонент AddTodo.


// AppState.ts
import { action as a } from 'mobx';
import { TodoListState } from 'components/TodoList';
import { ITodoItem } from 'components/TodoItem';

export class AppState {
  public todoList = new TodoListState();

  @a public addTodo = (value: string) => {
    const newTodo: ITodoItem = {
      id: Date.now().toString(),
      name: value,
      completed: false,
    };
    this.todoList.addTodo(newTodo);
  };
}

Компонент AddTodo имеет изолированный стейт. К нему нет доступа из общего стейта. Единственная связь с appState осуществляется через метод appState.addTodo при submit формы.
Для стейта компонента AddTodo используется библиотека formstate, которая отлично дружит с ts и mobx. Formstate позволяет удобно работать с формами, осуществлять валидацию форм и прочее. Форма имеет только одно обязательное поле name.


// AddTodoState.ts
import { FormState, FieldState } from 'formstate';

export class AddTodoState {
  // Create a field
  public name = new FieldState('').validators(
    val => !val && 'name is required'
  );

  // Compose fields into a form
  public form = new FormState({
    name: this.name,
  });

  public onSubmit = async () => {
    //  Validate all fields
    const res = await this.form.validate();
    // If any errors you would know
    if (res.hasError) {
      console.error(this.form.error);
      return;
    }
    const name = this.name.$;
    this.form.reset();
    return name;
  };
}

В целом, нет смысла описывать полностью поведение всех компонентов. Полный код приведен тут.


В данной статье приведена попытка автора писать простой, гибкий и структурированный код, который легко поддерживать. React делит UI на компоненты. В компонентах описаны классы стейтов (можно отдельно тестировать каждый класс). Экземпляры стейтов создаются либо в самом компоненте, либо уровнем выше, в зависимости от задач. Достаточно удобно, что можно указывать типы полей класса и типы свойств компонентов благодаря typescript. Благодаря mobx мы можем, практически незаметно для разработчика, заставить react компоненты реагировать на изменение данных.

Теги:
Хабы:
Всего голосов 17: ↑14 и ↓3+11
Комментарии8

Публикации

Истории

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань