Поиск правильного решения для управления состоянием в React становится все более сложной задачей. Узнайте, почему мы перешли с React Hooks на MobX, и какие основные преимущества в производительности мы получили.

Руи Лу, инженер React Native в Whitespectre

Проблемы с производительностью React Hooks

За последние два года мы разработали новое мощное приложение для управления первоклассным оборудованием GPS для SpotOn. Приложение позволяет владельцам собак создавать беспроводные GPS-заборы для своих питомцев и постоянно отслеживать их местоположение. При расширении функций отслеживания в реальном времени на карте мы столкнулись с некоторыми ограничениями React Hooks, нашей системы управления состоянием. Хотя хуки — это интегрированный, простой и четко определенный метод, который отлично подходит для управления локальными состояниями внутри одного компонента React, он стал неэффективным по мере развития функциональности приложения. С более обширным приложением количество постоянно обновляемых состояний, в свою очередь, росло, особенно при работе с наиболее ресурсоемкими функциями приложения.

Одной из таких функций является отслеживание хлебных крошек. Эта функция помогает владельцу собаки увидеть путь своей собаки на карте после побега из-за забора. Основываясь на геолокации пользователя на устройстве, нашей серверной службе и характеристиках Bluetooth, отправленных с собачьего ошейника SpotOn, приложение рисует на карте путь, по которому шла собака, и постоянно обновляет его в режиме реального времени.

В нашем первоначальном подходе мы инкапсулировали эти источники как состояния в коде и считывали их внутри компонентов, используя Context. Контекст медленный и неэффективный, потому что каждый компонент должен быть повторно отрендерен, даже если многие изменения не имеют к нему отношения. С изменениями частоты в реальном времени все могло только ухудшиться. И хотя нам нужно было, чтобы эта карта соответствовала нашим высоким стандартам — т. е. была гладкой и удобной для пользователя и минимально влияла на производительность и ресурсы — React Hooks вызывали мерцание, задержки и зависание.

И это была лишь одна из трудностей, с которыми мы столкнулись при использовании React Hooks. Мы можем перечислить еще как минимум три проблемы:

Методы с отслеживанием состояния, которые считывают или обновляют состояния, сложны в управлении

По мере развития кодовой базы часто становится непонятно, когда изменится метод с отслеживанием состояния и как это повлияет на useEffect, зависящий от него.

Перехватчики React ограничивают нашу гибкость в проектировании и структурировании кода

Мы вынуждены использовать хуки только внутри компонентов React или других функций хуков. Если вы хотите разделить состояния хуков между модулями и иерархиями, контекст и поставщик — единственный подходящий подход.

Постоянное исправление кода

Хотя мы хотим сосредоточиться на функциях, которые нравятся нашим клиентам и пользователям, с помощью React Hooks мы потратили много времени на отладку проблем и рефакторинг кода, чтобы сделать его максимально чистым.

Почему MobX был лучшим вариантом?

В настоящее время доступно множество популярных библиотек управления состоянием для проектов React. Все они направлены на улучшение управления государством, но каждый фокусируется на одной или нескольких конкретных областях. Поэтому речь идет не о выборе лучшей библиотеки для всех; вместо этого выберите тот, который лучше всего подходит для вашего проекта. В Whitespectre мы выбрали MobX для недавнего проекта, потому что он выделяется следующими способами:

Минимальные концепции

Мы можем засвидетельствовать, что девиз MobX — «Простое, масштабируемое управление состоянием» — верен на 100%. С MobX все, что вам нужно для начала, — это определить некоторые состояния, наблюдать за ними в компонентах и ​​обновлять их, используя старые добрые назначения Javascript. Никакой шаблонный код не является жизненно важным для команд, которые хотят, чтобы каждый разработчик был на борту с плавной кривой обучения и перехода.

Более высокая производительность

По мере развития проекта количество состояний, на которые ссылается иерархия компонентов, постоянно растет. Поскольку обновление компонентов — это задача, которая требует больших затрат времени и ресурсов устройства, возможность сократить количество ненужных обновлений компонентов может значительно повысить производительность. Часто нам нужно обновить только небольшое их подмножество, и MobX гарантирует, что обновляются только компоненты, которые ссылаются на эти состояния, в то время как остальная часть экрана остается неизменной.

Гибкость и совместимость

MobX позволяет обновлять состояния в любом месте кода, а не только внутри компонентов. Вы можете отделить состояние и логику от ваших компонентов в соответствии с любой архитектурой, которую вы предпочитаете. Более того, MobX хорошо работает с существующим кодом React Hooks, и один компонент может управляться как состояниями MobX, так и состояниями Hooks. В следующем разделе мы представляем шаблон реализации, который помог нам постепенно перейти на MobX, не нарушая остальной код, сохраняя масштабируемость проекта и простоту обслуживания по мере добавления новых функций и сложностей.

Как мы успешно перешли на MobX с хуков

Все переходы могут быть запутанными, если не обрабатываются должным образом, и даже больше для этой миграции управления состоянием, когда данные и состояния управляют всем приложением. Банальная ошибка при переходе может привести к серьезным проблемам с пользовательским интерфейсом. Мы рассмотрели кривую обучения, усилия по рефакторингу и совместимость нашего проекта. Выполнив следующие шаги, вы сможете постепенно переключаться между хуками и MobX, не нарушая работу ваших пользователей.

Определение магазинов MobX

Прежде чем мы перешли на MobX, мы управляли состояниями с помощью стандартного useState из React Hooks и делились ими между модулями и компонентами через Context. Например, для приложения со списком дел, в котором мы хотим отобразить список дел и разрешить пользователю добавлять новые или удалять существующие элементы, в итоге у нас будет файл с именем useTodo.tsx со следующей реализацией:

import React, { 
  createContext,
  FC,
  useCallback,
  useContext,
  useState
} from 'react';
type Item = {
  id: string;
  title: string;
};
type TodoContextValue = {
  items: Item[];
  addItem: (title: string) => void;
  removeItem: (id: string) => void;
};
export const TodoContext = createContext<TodoContextValue>(null!);
export const TodoProvider: FC<{}> = ({ children }) => {
  const [items, setItems] = useState<Item[]>([]);
  const addItem = useCallback(
    (title: string) => {
      setItems(items.concat({ id: Date.now().toString(), title }));
    },
    [items],
  );
  const removeItem = useCallback(
    (id: string) => {
      setItems(items.filter(item => item.id !== id));
    },
    [items],
  );
  const value = { items, addItem, removeItem };
  return 
    <TodoContext.Provider value={value} >
      {children}
    </TodoContext.Provider>;
};
export const useTodo = () => useContext(TodoContext);

Здесь у нас есть элементы, состояние, созданное с помощью useState, представляющее все элементы списка дел в приложении. Кроме того, у нас есть два метода, позволяющих добавлять новые элементы и удалять существующие элементы. Мы открываем их все дочерним компонентам TodoProvider, которые будут смонтированы в корневом дереве приложения. Вот как мы определяем группу связанных состояний и действий над ними и как мы делимся ими в проекте на основе хуков.

К счастью, MobX поощряет подобные шаблоны. Следуя разделу определение хранилищ данных MobX, мы определяем хранилища MobX для функциональных модулей и общих глобальных модулей. Мы можем реорганизовать реализацию хуков выше в хранилище MobX:

import { makeAutoObservable } from 'mobx';
type Item = {
  id: string;
  title: string;
};
export class TodoStore {
  items: Item[] = [];
  constructor() {
    makeAutoObservable(this);
  }
  addItem = (title: string) => {
    this.items = this.items.concat({ id: Date.now().toString(), title })
  };
  removeItem = (id: string) => {
    this.items = this.items.filter(item => item.id !== id);
  };
}

Здесь мы сохранили те же функциональные возможности этого приложения списка дел, которые включают элементы, addItem() и removeItem(), но MobX теперь управляет ими. Строка makeAutoObservable(this) в конструкторе является ключевой частью. Он будет отображать все свойства (в данном случае элементы) как часть вашего состояния, а все ваши функции — как действия, изменяющие это состояние. Другие части кода могут либо наблюдать за частью состояния, либо выполнять действия по его изменению. Обратите внимание, что addItem() и removeItem() теперь являются обычными стрелочными функциями Javascript, что означает, что вы можете использовать их так же, как и любые другие функции JS, не беспокоясь о зависимостях! Напротив, в предыдущей реализации хуков они представляли собой функции с состоянием, обернутые useCallbacks, которые требуют вашего внимания всякий раз, когда вы их используете.

В процессе миграции основная работа заключалась в рефакторинге состояний хуков в хранилищах MobX. Благодаря схожим концепциям с обеих сторон это было очень просто. После того, как у нас были готовы магазины, нам нужно было установить связи между магазинами и компонентами.

Чтение хранилищ MobX из компонентов React

При стандартной реализации хуков чтение состояния внутри компонента React выполняется путем извлечения значения из контекста. Сначала вставляем TodoProvider в корневое дерево:

const App = () => {
  return (
    ...
      <TodoProvider>
        ...
      </TodoProvider>
    ...
  );
};
export default App;

Затем мы извлекаем элементы из useTodo и отображаем их по мере необходимости:

const List = () => {
  const { items } = useTodo();
  return (
    <>
    {
      items.map(item => {
        return <Item item={item} />
      })
    }
    </>
  );
};

Что насчет МобХ? В нашем проекте есть десятки файлов хуков React и еще больше компонентов, которые их читают. Нашей целью было свести к минимуму (или исключить) утомительную работу по внесению изменений на стороне компонентов.

TodoStore, определенный выше, представляет собой простой класс Javascript, и MobX не ограничивает нас в том, как мы структурируем код. Мы можем использовать тот же контекстный подход для обмена хранилищами MobX точно так же, как мы делаем с состояниями хуков:

import { makeAutoObservable } from 'mobx';
import React, { createContext, FC, useContext, useState } from 'react';
type Item = {
  id: string;
  title: string;
};
export class TodoStore {
  items: Item[] = [];
  constructor() {
    makeAutoObservable(this);
  }
  addItem = (title: string) => {
    this.items = this.items.concat({ id: Date.now().toString(), title })
  };
  removeItem = (id: string) => {
    this.items = this.items.filter(item => item.id !== id);
  };
}
export const TodoContext = createContext<TodoStore>(null!);
export const TodoProvider: FC<{}> = ({ children }) => {
  const [store] = useState(new TodoStore());
  return <TodoContext.Provider value={store}>{children}</TodoContext.Provider>;
};
export const useTodo = () => useContext(TodoContext);

Под определением магазина мы добавили несколько строк для создания контекста и поставщика для этого магазина. Единственное отличие состоит в том, что здесь мы делимся только одним экземпляром объекта хранилища в качестве значения и не обновляем себя на протяжении всего жизненного цикла TodoProvider. Если вы посмотрите, эти строки в основном представляют собой шаблон, который можно скопировать и вставить для любых других новых магазинов MobX.

Последний шаг — использовать observer() из привязок MobX для React, чтобы обернуть все компоненты, и готово! Без каких-либо изменений в компонентах мы завершили миграцию, подменив реализацию TodoProvider из чистых состояний Hooks в хранилище MobX.

Можете ли вы сказать, является ли приведенный ниже код useTodo набором состояний хуков или хранилищем MobX?

const List = observer(() => {
  const { items } = useTodo();
  return (
    <>
    {
      items.map(item => {
        return <Item item={item} />
      })
    }
    </>
  );
});

Заключительные мысли о MobX

1. Мы любим Hooks и любим до сих пор! Хуки отлично подходят для управления локальными состояниями внутри одного компонента React. Более того, если вы пишете библиотеку для общедоступных или внутренних команд, Hooks поможет вам создать чистую кодовую базу, подходящую для большинства проектов.

2. MobX и Hooks могут жить спокойно. Часто требуются огромные усилия и время, чтобы преобразовать весь проект в новое решение, особенно с управлением состоянием, которое управляет всеми данными, отображаемыми на экране. С реализацией, описанной в этой статье, вы можете постепенно переходить от хуков к MobX, не нарушая работу ваших пользователей.

3. Управление состоянием с помощью MobX простое и увлекательное. Если вы хотите внедрить его в свои проекты React, зайдите на веб-сайт MobX, где вы можете найти основы и профессиональные советы, не описанные в этой статье, чтобы получить максимальную производительность и чистый код без ущерба друг для друга.