Typescript and react

Javascript development sometimes becomes similar to detective work. How to understand someone else's code? It’s good if the developer has the subtle skill of naming variables so that others understand the point. But what if the team members are still not always able to understand the intent of their colleague? How to understand what comes into the argument of a function?







Suppose the function argument is called errors. Probably in the errors is an array. Most likely lines? Well, that array is understandable. After all, its length is further checked. But the string property also has a length property. It seems that to figure it out exactly, you need to put a breakpoint and run the script. Then completely go through the script on the UI (for example, we need the final step of the form). Now in devtools it can be seen that errors is an object with a set of specific fields, including the length field.







Such ambiguity in parsing javascript code leads to a waste of developer time. A good solution in this case could be typescript (hereinafter ts). You can use it in the next project, or better yet, make ts support in the existing one. After that, the time to understand someone else's code will be significantly reduced. Indeed, in order to understand the structure of any data, one click is enough. You can concentrate on the logic of working with data and at any time know that you clearly understand the code.







Some advantages of ts should be noted. It is widely used in various frameworks and is closely related to javascript. The development of ts is determined by the needs of frontend developers.







This article presents the development of todo applications, but only a brief description of interesting points. Full code can be found here .







I used react, typescript and mobx. Mobx is a flexible tool for managing application state. Mobx is concise. It allows you to work with the state of react components in a synchronous style. No problem like:







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





In this case, the old state.name is displayed.







In addition, mobx is convenient and does not interfere with working with ts types. You can describe state as separate classes or directly inside the react component.







For simplicity, all components are placed in the components folder. A component is defined in the component’s folder with a description of the state that is logically associated with the display and operation of the component.







The TodoItem folder contains a file with the react component TodoItem.tsx, a file with the TodoItem.module.scss styles, and a TodoItemState.ts status file.







TodoItemState.ts describes the fields for storing data, how to access them, and the rules for changing them. The range of possibilities is very large thanks to OOP and ts. Part of the data can be private, part is read-only and so on. Using the @o decorator, observable fields are specified. React components react to their changes. The @a ( action ) decorators are used in methods to change state.







 // 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; }; }
      
      





In TodoItem.tsx, only two properties are passed to props. In mobx, it is optimal for the overall performance of the application to transfer complex data structures to the props react component. Since we use ts, we can precisely indicate the type of object accepted by the component.







 // 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> ); } }
      
      





The ITodoItemProps interface describes the todo property of type TodoItemState. Thus, inside the react component, we are provided with data for display and methods for changing them. Moreover, restrictions on changing data can be described both in the state class and in the methods of the react component, depending on the tasks.







The TodoList component is similar to TodoItem. In TodoListState.ts, you can see getters with the @c (@computed) decorator. These are ordinary class getters, only their values ​​are memoized and recounted when their dependencies change. Computed by design is similar to redux selectors. Conveniently, it is not necessary, like React.memo or reselect, to explicitly pass a list of dependencies. React components respond to computed changes as well as observable changes. An interesting feature is that the recalculation of the value does not occur if computed is not currently involved in the rendering (which saves resources). Therefore, despite maintaining constant dependency values, computed can be recalculated (there is a way to explicitly tell mobx that you need to save the computed value).







 // 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; }; }
      
      





Access todo list is open only through a computed field where, depending on the viewing mode, the necessary filtered data set is returned (completed, active or all todo). The compiled fields completedTodos, activeTodos, and the private observable field _todos are indicated in the todo dependencies.







Consider the main component of the App. It renders a form for adding new todo and a todo list. An instance of the main AppSate state is instantly created.







 // 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> ); } }
      
      





The appState field contains an instance of the TodoListState class to display the TodoList component and the method for adding new todo, which is passed to the AddTodo component.







 // 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); }; }
      
      





The AddTodo component has an isolated state. There is no access to it from the general state. The only connection to appState is through the appState.addTodo method when submitting a form.

For the AddTodo component's state, the formstate library is used, which is great friends with ts and mobx. Formstate allows you to conveniently work with forms, validate forms, and more. The form has only one required field 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; }; }
      
      





In general, it makes no sense to fully describe the behavior of all components. The full code is given here .







This article describes the author’s attempt to write simple, flexible, and structured code that is easy to maintain. React divides the UI into components. The components describe state classes (each class can be tested separately). Instances of states are created either in the component itself or at a higher level, depending on the tasks. Conveniently enough, you can specify class field types and component property types thanks to typescript. Thanks to mobx, we can, almost imperceptibly for the developer, make react components react to data changes.








All Articles