Сегодня мы собираемся написать простой список дел с помощью React Hooks и MobX.

Я дам краткое введение в эти библиотеки для людей, которые с ними не знакомы. Если вы это уже знаете, можете пропустить этот раздел.

Реагировать

Библиотека JavaScript для создания пользовательских интерфейсов, представленная Facebook в 2013 году и используемая для создания пользовательского интерфейса для веб-приложений. В отличие от Angular и Vue, React не является фреймворком и предоставляет только инструменты для отображения компонентов пользовательского интерфейса.

Реагировать на хуки

Для людей, которые уже знакомы с React, хуки - это расширение функциональных компонентов, более подробную информацию вы можете прочитать по ссылке.

MobX

MobX - это проверенная на практике библиотека, которая делает управление состоянием простым и масштабируемым за счет прозрачного применения функционального реактивного программирования (TFRP). Философия MobX очень проста (из документации).

Теперь, когда мы знаем, что будем использовать, мы можем приступить к работе над проектом.

Давайте откроем новый проект React. Существует большой стартовый проект под названием Create React App, который поддерживается сообществом и имеет все необходимые функции для запуска нового приложения React. (Настройка нового проекта React может занять время, поэтому неплохо иметь этот шаблон). Мы также будем использовать для этого TypeScript :)

Откройте терминал и напишите следующие команды:

  • Установите приложение create-response-app с машинописным текстом
  • Установите типы, необходимые для машинописного текста (в разделе devDependencies, поскольку мы не используем их во время выполнения)
  • Установите mobx и его подключение к React
npx create-react-app my-app --template typescript
npm install --save-dev typescript @types/node @types/react @types/react-dom @types/jest
npm i mobx mobx-react-lite

MobX использует декораторы, поэтому нам нужно будет добавить в наш tsconfig следующую строку "experimentalDecorators": true.

Давайте настроим наши магазины mobx и подключим их к нашему проекту React. Нашими первыми шагами будут:

  • Создать TodoList и TodoItem
  • Создайте контекст и оберните наше приложение его поставщиком, чтобы поделиться магазинами mobx
  • Создайте способ получить магазин из контекста
export class TodoList {
    @observable.shallow list: TodoItem[] = [];

    constructor(todos: string[]) {
        todos.forEach(this.addTodo);
    }

    @action
    addTodo = (text: string) => {
        this.list.push(new TodoItem(text));
    }

    @action
    removeTodo = (todo: TodoItem) => {
        this.list.splice(this.list.indexOf(todo), 1);
    };

    @computed
    get finishedTodos(): TodoItem[] {
        return this.list.filter(todo => todo.isDone);
    }

    @computed
    get openTodos(): TodoItem[] {
        return this.list.filter(todo => !todo.isDone);
    }
}

Мы создаем класс TodoList, содержащий массив @observable из TodoItem. Обратите внимание, что мы присваиваем ему начальное значение и делаем наблюдаемое неглубоким, чтобы оно не переносило значения внутри массива. Мы заставляем его получать массив строк для инициализации нашего списка и добавления 2 простых функций добавления и удаления, мы используем стрелочные функции для действий, потому что мы полагаемся на this, и мы не хотим терять контекст при выполнении действий todos.forEach(this.addTodo) на Функции получения, мы не используем стрелочные функции, потому что мы используем их только как средство получения.

export default class TodoItem {
    id = Date.now();

    @observable text: string = '';
    @observable isDone: boolean = false;

    constructor(text: string) {
        this.text = text;
    }

    @action
    toggleIsDone = () => {
        this.isDone = !this.isDone
    }

    @action
    updateText = (text: string) => {
        this.text = text;
    }
}

Мы создаем простой класс TodoItem и даем ему 2 наблюдаемых свойства текста и isDone и 2 функции для переключения его статуса и обновления текста.

Теперь воспользуемся React Context. Это позволяет нам обмениваться данными между всеми компонентами React внутри поставщика, который он нам дает, мы создадим контекст, используя createContext из React, и передадим ему значение по умолчанию для пустого объекта и назначим ему тип нашего хранилища TodoList (обычно в большой проект может быть корневым хранилищем).

import { createContext } from 'react';
import {TodoList} from "../stores/todo-list";

export const StoreContext = createContext<TodoList>({} as TodoList);
export const StoreProvider = StoreContext.Provider;

Мы переходим к нашему index.tsx файлу, создаем наше хранилище списков дел и обертываем или <App /> компонент с поставщиком, который мы получили из нашего контекста, и передаем ему todoList в качестве значения.

const todoList = new TodoList([
    'Should Starting Writing in React',
    'Should Learn MobX',
    'Should Watch Once Piece :)'
]);

ReactDOM.render(
    <StoreProvider value={todoList}>
        <App/>
    </StoreProvider>
    , document.getElementById('root'));

Наконец, мы добавляем функцию, которая поможет нам получить хранилища внутри функций React. Используя useContext React, мы передаем ему контекст, который мы создали ранее, и получаем значение, которое мы предоставили (todoList).

export const useStore = (): TodoList => useContext(StoreContext);

Теперь мы можем приступить к написанию наших компонентов перехватчиков реакции 🎆. Мы собираемся создать 3 компонента:

  • Список дел
  • TodoItem
  • TodoNew

Начнем с TodoList:

export const TodoList = () => {
    const todoList = useStore();

    return useObserver(() => (
        <div className="todo-list">
            <div className="open-todos">
                <span>Open Todos</span>
                {todoList.openTodos.map(todo => <TodoItem key={`${todo.id}-${todo.text}`} todo={todo}/>)}
            </div>
            <div className="finished-todos">
                <span>Finished Todos</span>
                {todoList.finishedTodos.map(todo => <TodoItem key={`${todo.id}-${todo.text}`} todo={todo}/>)}
            </div>
        </div>
    ));
};

Мы используем нашу вспомогательную функцию useStore для получения списка задач, затем мы просто возвращаем 2 списка завершенных и незавершенных задач из вычисленных значений хранилища списков задач.

Здесь важно использовать useObserver из mobx-react-lite, чтобы обернуть возвращаемое значение, чтобы mobx знал, что нужно отслеживать наблюдаемые внутри значения. Без этого, если вы попытаетесь обновить список извне, это не сработает (подробнее об этом можно прочитать здесь https://mobx-react.js.org/observe-how).

export const TodoItem = ({todo}: Props) => {
    const todoList = useStore();
    const [newText, setText] = useState('');
    const [isEditing, setEdit] = useState(false);

    const saveText = () => {
      todo.updateText(newText);
      setEdit(false);
      setText('');
    };

    return (
        <div className="todo-item">
            {
                isEditing ?
                    <div>
                        <input type="text" onKeyDown={onEnterPress(saveText)} onChange={(e) => setText(e.target.value)}/>
                        <button onClick={saveText}>save</button>
                    </div>
                    :
                    <div>
                        <span>{todo.text}</span>
                        <input type="checkbox" onChange={todo.toggleIsDone} defaultChecked={todo.isDone}></input>
                        <button onClick={() => setEdit(true)}>edit</button>
                        <button onClick={() => todoList.removeTodo(todo)}>X</button>
                    </div>
            }
        </div>
    )
};

Мы создаем TodoItem, который получает в props модель класса mobx для задачи. Мы даем компоненту базовые возможности по редактированию, удалению и обновлению его текста. Здесь важно увидеть, как мы позволяем задаче обновлять сам текст, и нет необходимости обновлять для этого весь список (я добавил вспомогательную функцию onEnterPress, чтобы упростить проверку ввода ключей), здесь мы не использовали useObserver потому что todo уже наблюдается внутри TodoList.

Компонент TodoNew, который вы можете увидеть в репозитории Github. Я не буду добавлять его сюда, потому что это те же концепции и код.

Репозиторий Github: https://github.com/stolenng/react-hooks-mobx

Подводя итог, это простое руководство показывает, как мы соединяем react и hooks с mobx с помощью mobx-react-lite. Я думаю, что эта комбинация компонентов функции React (хуков) с mobx дает отличный код, очень простой и понятный. Вы можете использовать это как стартовый проект с mobx и настраивать его по своему усмотрению.

Обновление от 20 сентября

Я выпустил обширный курс по MobX, вы можете проверить его здесь:



Если у кого-то есть вопросы или просто хочется поговорить на эту тему, добро пожаловать в личку :)