Typescript и react

Разработка на 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 может пересчитаться (cсуществует способ явно указать 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 компоненты реагировать на изменение данных.

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *