Создание простого приложения с помощью этой отличной альтернативы Redux

Последние 6 лет React был моим основным решением для разработки веб-интерфейсов. Его концепции просты для понимания, и мне также нравится, как он позволяет мне создавать свой собственный стек на его основе для создания интерфейса внешнего интерфейса, который мне нужен, специально адаптированный для каждого случая использования.

Однако за такую ​​гибкость приходится платить - существует так много вариантов для покрытия каждого аспекта приложения, что невозможно начать экспериментировать со всеми из них, чтобы выяснить, какая технология является наиболее подходящей для веб-сайта.

Решения по управлению состоянием для React

В настоящее время интерфейсные веб-приложения больше похожи на настольные приложения, чем на старые приложения. Состояние сохраняется в переменных внутри кода, выполняемого в браузере. С появлением одностраничных приложений на базе фреймворков React и Angular глобальное управление состоянием стало приобретать все большее значение. Кроме того, интерфейсные приложения становятся все больше и больше с течением времени и развитием отрасли. Необходимостью стало сохранение пользовательских предпочтений, выбор всего сайта и реактивный пользовательский интерфейс. Очень немногие вещи имеют большее влияние на ваш проект, чем выбор управления состоянием.

Последние 5 лет или около того Redux (разработанный в 2013 году) был фактическим стандартом при рассмотрении системы государственного управления. Это изменило способ разработки приложений React (хотя он не зависит от фреймворка). Это позволило масштабировать приложение React до ранее неуправляемого уровня из-за его сложности с управлением состоянием и передачей свойств между компонентами в разных местах внешнего интерфейса.

Его массовое внедрение стало настолько распространенным, что многие люди не понимают, почему они его используют, а плохие практики повсюду. Настолько, что создатель Redux официально опубликовал в блоге сообщение о том, что Redux - не панацея и что он может вам не понадобиться.

… И я должен это признать: я бы не сказал, что мне очень нравится Redux. Я до сих пор использую его во многих проектах - мне нравится, как он интегрируется с React и его хуками. Однако есть альтернативы, такие как:

Я уверен, что что-то забыл. И я не скажу вам, что использовал их все. Но я широко использовал Mobx и Mobx-state tree, и меня удивило, что они не более популярны, чем сегодня.

Почему Mobx-state-tree?

MobX-state-tree - это самоуверенная структура управления состоянием, основанная на MobX. На самом деле, это может быть наиболее категоричная из всех альтернатив, что не обязательно плохо.

Лично я использую mobx-state-tree в основном потому, что хочу:

  • Знайте, где находится код, обрабатывающий состояние - это очень важно для меня, что очень раздражает меня в Redux, поскольку код состояния разделен между редукторами, создателями действий и т. Д. С помощью MobX State Tree (теперь MST ), ваш код будет кратким и собранным в одном месте.
  • Код предсказуем и понятен. С MST вы никогда не останетесь задаваться вопросом, как вы структурируете свои модели или структурируете свои модели для обновления пользовательского интерфейса. MST использует свои собственные типы данных, заставляя вас создавать деревья структур данных.
  • Это строго типизировано. Даже при использовании Javascript данные, содержащиеся в модели, проверяются на тип. Интеграция с Typescript тоже очень проста.

Различия между Mobx и Mobx State Tree

  • MST очень самоуверен. Это заставляет вас структурировать ваши модели определенным образом и использовать их, используя концепции библиотеки. Это одна из причин, по которой я предпочитаю MST обычному Mobx, но я понимаю, что это личное предпочтение.
  • MST проверяет типы свойств вашей модели даже при использовании простого Javascript, тогда как обычный Mobx этого не делает. Это может привести к небольшому снижению производительности (хотя у меня нет конкретных данных для подтверждения этого, и я не видел никаких показателей, показывающих что-то подобное)
  • Дизайн MST вращается вокруг централизованной модели, содержащей подмодели. Вы используете эти подмодели в своих компонентах. В некотором смысле это больше похоже на централизованное хранилище, как это делает Redux. Mobx не налагает подобных ограничений, и вы можете создать столько магазинов, сколько захотите, и использовать их где угодно.

Простое приложение

Давайте создадим простое приложение, которое будет делать следующее:

  • Используйте API https://restcountries.eu/, чтобы получить список доступных стран и представить их. Это даст представление о том, как выполнять асинхронные действия с MST.
  • Обновляйте этот список каждые 60 секунд. Это позволит нам изучить, как использовать изменчивые переменные.
  • Список будет обновлен вручную путем нажатия кнопки или установки временного интервала, который может быть аннулирован посредством ввода пользователем.

Чтобы создать наше простое приложение, мы будем использовать Typescript. Мы также не будем тратить время на форматирование / улучшение нашего приложения. Единственная цель нашего примера - показать использование MST в приложении React.

Начнем с кода, который настраивает хранилище, в котором будут храниться все переменные внутри.

Строительные леса для приложений

Мы начнем использовать простое шаблонное приложение Create React App, настроенное на использование машинописного текста,

npx create-react-app my-app --template typescript
# or
yarn create react-app my-app --template typescript

затем нам нужно будет добавить несколько компонентов, которые нам понадобятся для разработки остальной части приложения:

npm install --save axios mobx-react-lite mobx mobx-state-tree
# or
yarn add axios mobx-react-lite mobx mobx-state-tree

Настройка магазина

Следующая модель будет изображать каждый пункт страны. Обратите внимание, что в полном API, предлагаемом RESTCountries, есть гораздо больше переменных, но я решил не включать их для ясности.

interface Country {
  name: string;
  alpha2Code: string;
  alpha3Code: string;
  callingCodes: string[];
  capital: string;
  region: string;
  population: number;
  area: number;
  gini: number;
  borders: string[];

  currencies: Array<{
    code: string;
    name: string;
    symbol: string;
  }>;
}

Мы создадим два хранилища MST, в одном из которых будут храниться страны, а в другом - хранилище стран (корневое хранилище). В целом ожидается, что модели MST будут встроены в другие модели и будут использоваться RootStore, и именно этим мы и собираемся заняться здесь (хотя это может быть немного ненужным для такого небольшого упражнения).

const CountryStore = types.model({
  countries: types.array(types.frozen<Country>()),
});

export const RootStore = types.model({
  countryStore: CountryStore,
});

Здесь мы используем types.frozen как средство удержания ответа API. frozen предназначен для хранения значения, которое может быть сериализуемым и неизменным. Ради этого примера мы будем рассматривать его так - поскольку мы не собираемся изменять какую-либо часть ответа, но ответ действительно является объектом JSON, состоящим из простых значений (таким образом, сериализуемым).

export function initializeStore() {
  _store = RootStore.create({
    countryStore: { countries: [] },
  });
  return _store;
}

Мы намерены передать RootStore из корневого элемента нашего приложения всем остальным элементам нашего приложения. Для этого мы будем использовать Provider API React.

const RootStoreContext = createContext<null | RootInstance>(null);
export const Provider = RootStoreContext.Provider;

Затем мы сможем внедрить его в любой элемент, который мы собираемся использовать, через useContext. Давайте напишем метод для безопасного извлечения этого (проверяет, правильно ли инициализировано хранилище.

export function useStore(): Instance<typeof RootStore> {
  const store = useContext(RootStoreContext);
  if (store === null) {
    throw new Error("Store cannot be null, please add a context provider");
  }
  return store;
}

Полный код начальной версии thestore.ts находится здесь:

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

Внутри нашего App.ts:

const store = initializeStore();

function App() {
  return (
    <Provider value={store}>
      <div className="App">
        <CountriesList />
      </div>
    </Provider>
  );
}

export default App;

Обратите внимание на использование CountryList внутри класса. Это компонент, который представит список стран и будет обращаться к Store для этого.

Каждый компонент, которому необходимо использовать свойства магазина для наполнения пользовательского интерфейса данными, должен быть observer. Чтобы сделать это наблюдателем, нам нужно импортировать компонент высшего порядка наблюдателя из mobx-react-lite. Теперь мы можем использовать метод useStore, который мы создали в файле store.ts, чтобы получить экземпляр нашего магазина. Помните, что хранилище передается любому компоненту с помощью Provider API React. Затем мы можем использовать свойства магазина в обычном режиме, как если бы это был любой другой наблюдаемый объект.

Вам может быть интересно, что это за countryStore.refreshCountries() мы используем внутри нашего компонента. Это действие, которое извлекает данные. Подробнее об этом чуть позже.

Мы запускаем наше приложение и теперь видим пустую страницу. Это верно - мы еще не получили никаких данных.

Заполнение нашего списка данными

Что нам теперь нужно сделать, так это заполнить наш CountryStore данными, полученными из фида. Мы будем использовать Axios для выполнения наших запросов и получения данных с restcountries.eu веб-сайта. Помните этот countryStore.refreshCountries(), о котором мы говорили ранее? Пришло время написать для него код, чтобы мы могли использовать его внутри нашего компонента.

Нам нужно будет выполнить асинхронную операцию, чтобы получить данные и сохранить их в нашем CountriesStore. Имейте в виду, что в MST свойства модели нельзя изменить из любого места. Самоуверенный характер MST заставляет нас изменять состояние из actions метода. actions’ аргумент - это обратный вызов, который передает аргумент вызывающей стороне (назовем его self), который на самом деле является объектом, имеющим изменяемые свойства магазина. Так что мы можем сделать self.countries=… без жалоб со стороны MST.

Более того, когда нам нужно иметь дело с асинхронной операцией, нам нужно уделять особое внимание при написании наших действий. Mobx объясняет, почему в документации.



Вот в чем суть: если вы хотите внести изменения в свойства модели MST, у вас есть следующие возможности:

  • Используйте старые добрые обещания и вызывайте отдельные действия для установки нового значения свойств модели.
  • Используйте генераторы для моделирования использования async-await операции.

Мы выберем второе, так как это сохранит лаконичность кода. Если нет особой технической причины следовать методу 1, я всегда предпочитаю использовать функцию генератора, заключенную в flow HOC.

export const CountryStore = types
  .model({
    countries: types.array(types.frozen<Country>())
  })
  .actions((self) => {
    const refreshCountries = flow(function* () {
      console.log("refreshing countries...")
      const response: Country[] = yield axios
        .get("https://restcountries.eu/rest/v2/all")
        .then((value) => value.data);

      self.countries = cast(response);
    });

    return { refreshCountries };
  });

Обратите внимание на код внутри actions и обратите внимание, что refreshCountries - это функция генератора, заключенная в функцию flow высшего порядка. Также обратите внимание, что yield ведет себя как await ключевое слово и принимает Promise, что позволяет нам выполнять любые асинхронные операции внутри действия Модели.

Если мы нажмем «Обновить» в браузере, мы увидим, что список заполнен. Наш компонент рисует элементарный и уродливый список названий стран, асинхронно извлекаемых REST API.

Мы могли бы остановиться здесь. Однако есть еще один аспект MST, который я хотел бы осветить, поскольку для меня это было нетривиальным открытием.

Добавление автообновления в магазин

Давайте добавим функцию и новый раздел внутри нашего небольшого приложения, которое будет иметь следующие функции:

  • Покажите счетчик, показывающий, сколько раз мы вызывали REST API для обновления списка стран.
  • Разрешить - установить и отключить таймер, который будет автоматически обновлять список каждые X секунд.

Способ хранения нужных нам данных не так прост, как кажется. Нам нужно будет сохранить intervalId, чтобы мы могли установить или отменить его с помощью setInterval. Мы не будем удерживать его с остальными нашими свойствами, потому что это ненаблюдаемое значение. Это непостоянное значение. И у MST это понятие описано в документации.

actions можно объединить в цепочку, что является прекрасным способом добавить некоторые действия, которые управляют своим собственным состоянием и имеют некоторые частные значения, к которым мы не хотим, чтобы кто-либо другой имел доступ.

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

export const CountryStore = types
  .model({
    countries: types.array(types.frozen<Country>()),
    timesRefreshed: types.number,
    autoRefreshActive: types.boolean,
  }

Затем давайте добавим еще один раздел actions, в котором будет настроено и отключено автоматическое обновление.

Обратите внимание на использование timeIntervalId. Он содержит идентификатор интервала таймера Javascript. Мы решили позволить этой переменной существовать как локальная область видимости внутри раздела actions, поскольку она не наблюдаема, и мы хотим, чтобы она была частной. В терминах MST это также называется «локальной переменной».

Нам также нужно будет обновить наш метод refreshCountries, чтобы увеличивать счетчик timesRefreshed каждый раз, когда происходит обновление.

const refreshCountries = flow(function* () {
  const response: Country[] = yield axios
    .get("https://restcountries.eu/rest/v2/all")
    .then((value) => value.data);
  self.countries = cast(response);
  self.timesRefreshed += 1;
});

Теперь давайте добавим новый компонент с именем ListRefreshControl, в обязанности которого будет входить управление обновлением списка стран и установка / отключение таймера для этого.

И наконец, давайте обновим наш App Компонент, чтобы он соответствовал этому новому Компоненту.

Запустим приложение.

Пришло время запустить приложение. Посетите localhost:3000 в своем браузере, и вы увидите следующее изображение.

Нажатие кнопки «Обновление вручную» обновит список стран и увеличит счетчик «Время обновления» вверху страницы. Если вы нажмете кнопку «Настроить автоматическое обновление», она будет отключена, и таймер запустится. Список будет автоматически обновляться каждые 10 секунд. Нажатие кнопки «Слезить автообновление» приведет к аннулированию таймера, и автоматическое обновление остановится.

Полный store.ts

Для удобства ниже представлен исходный код файла store.ts, содержащего все, о чем мы говорили.

Заключение

MobX-state-tree - отличная альтернатива Redux, обеспечивающая хорошую поддержку Typescript, проверку типов для Javascript, лаконичность и отличную интеграцию с React. Я обнаружил, что использую его все чаще, и это помогло мне значительно уменьшить размер моей кодовой базы, сохранив при этом все функции, которые потребуются для разработки полнофункционального приложения.

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

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

Использованная литература: