Pull to refresh

Comments 56

Что забавно, MobX - единственный из списка, с кем реально пришлось помучаться)

MobX - единственный из списка, с кем реально пришлось помучаться)

Как именно?

state.count++;
state.title = 'new title';

Где мучения? Все кто следил за state.count и state.title автоматически перендерились.

const myComp = observer(() => {
return <div>count: {state.count} / title: {state.title}</div>
})

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

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

Фраза про "мучения" не должна быть воспринята буквально. То есть, все стейт менеджеры из статьи это решения, доказавшие свою эффективность на огромном числе проектов, redux и mobx особенно.

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

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

Опытная команда, уверен, избежит этого, но таких, как мы видим, меньшинство.

Тоже интересно, в чем были мучения, вроде все просто

Ответил выше)

Интересно ваше мнение

mobx позволяет мне писать в ООП парадигме реактивный код, не требует иммутабельности, вводит минимум когнитивной нагрузки. Остальные плюсы для меня проистекают из использования ООП как такового, а не из библиотеки для реактивности.

Что касается других библиотек, они либо повышают порог входа в проект при сомнительных плюсах (rxjs, например, кривая обучения резко возрастает, а плюсы в виде фп и операторов для декларативности в большинстве случаев избыточны, я уверен есть кейсы в которых rxjs показывает себя отлично, но сомневаюсь, что их много, на своем опыте могу сказать что rxjs скорее просто усложнял разработку, чем давал какие-то плюсы). Либо имеют невыразительное, многословное апи. Либо накладывают жесткие ограничения на способы создания реактивных сущностей. То есть натянуть редакс на ооп не получится, так как архитектурно они спроектированы так, что не позволяют тебе использовать их для того, для чего они были созданы в желаемой тобой парадигме без костылей. Нет, можно наверное как-то извратиться и закостылять что-то, но это будет мало приятно использовать. Так вот другие библиотеки страдают минимум одним из этих недугов, в основном конечно неудобное апи и ограничения на применение. Вопросы к производительности я опустил

Пользуюсь rtk redux + query, потому что есть кодогенерация апи в стор. Как по мне - имба.

Как с этим у mobx?

Отлично с этим у mobx, есть привязка к tanstack-query, который умеет на голову выше, чем rtk-query

А как обстоят дела с созданием динамических сторов в redux ? С реализацией MVVM паттерна ? Или аналогов библиотеки mobx-web-api (утилиты для реактивной работы с веб апи браузера)?

На Mobx можно писать так же, как в примере на Valtio, используя вместо proxy функцию observable. Единственное отличие в коде будет в том, что Mobx подключается к реактовым компонентам через враппинг в HOC observer, а Valtio - через useSnapshot. Плюсы Mobx подхода с враппингом в том, что можно использовать хоть 100 разных реактивных состояний в компоненте напрямую, без вызова для каждого useSnapshot, а сам враппинг можно сделать автоматически через бандлер, то есть компоненты в коде будут выглядеть чистыми.

Вариант с классовыми сторами просто более удобен в плане DX, поэтому часто выбирают его (меньше импортов и констант, меньше коллизии имен, не важен порядок объявления методов и геттеров). Но можно писать и объектами

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

В Redux/Zustand перерисовка явно связана с вызовом setState или dispatch, что с ростом масштаба кратно упрощает дебаг. Плюс DevTools у Redux (с логом действий и diffами состояния) гораздо мощнее, чем у MobX.

MobX очень гибок, у него этого не отнять. Но есть ощущение, что часто эта гибкость стреляет в ногу с ростом масштаба.

Безусловно, я далек от экспертности в контексте таких эджкейсов MobX. Если у вас есть опыт, как такое реашется, поделитесь, пожалуйста.

В Mobx тоже перерисовка связана явно, только вместо dispatch с полным пересозданием структуры здесь просто мутация store.param = value. Это дает намного более удобный DX - можно в IDE посмотреть все места, где читается/изменяется конкретное свойство и работают быстрые переходы.

А в Redux нужно сначала найти нужный initialState, потом найти десяток редюсеров, которые меняют в нем param, потом найти action name, по нему сделать полный поиск по проекту, чтобы найти все места где делается dispatch с этим названием экшена, не забыть пройтись по селекторам и connect HOC, чтобы узнать, где используются конкретный стор, а потом в каждом найденном компоненте искать где читается param. К сожалению, довольно много работал с Redux и пол-дня состояло как раз из этих действий, потому что никаких быстрых переходов или Find Usages у него не предусмотрено.

И с Redux в итоге не так-то просто найти, "почему компонент перерендерился", потому что нужно смотреть в Dev Tools и вручную искать какой именно из диспатчей данных повлиял на конкретный компонент, проходя все этапы, описанные выше. Но да, в нем есть диффы состояний, которые немного могут упростить процесс.

Дебажить кейс "почему компонент перерендерился" в целом непросто в любом подходе, и помогает тут скорее создание более атомарных компонентов, а не монстров с десятками состояний. Но с Mobx это как правило не нужно - компонент ререндерится при изменении читаемых свойств и если он перерендерился - значит нужно. Это не реактовый или редаксовый стейт, где все ререндерится даже если нечитаемые поля изменились, если тщательно не оптимизировать все через селекторы. Поэтому такой проблемы и не возникает

Это на самом деле очень хороший комментарий, спасибо за информацию!

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

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

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

Я тоже работал в проектах, где на Mobx создавали абсолютно неподдерживаемого монстра, используя все возможные антипаттерны. Например, использовали автонормализацию snake_case -> camelCase при получении данных из сторонних источников, что ломало быстрые переходы и поиск по проекту. Делали огромные Rich Store на тысячи строк, неконтролируемые autorun во всех возможных местах, использовали одновременно реактивные состояния и useState/useEffect, причем пытаясь из их комбинации сделать computed. По структуре тоже была каша - и Entity Stores, и сервисы со своими сторами, и модульные, и глобальные в несколько уровней.

Так что ваш аргумент тоже корректен - чем более гибок инструмент, тем легче писать некачественный код. Но вот еще один поинт - из Mobx очень легко сделать Redux. Просто добавить в ваш пример в статье метод dispatch(action, payload) , который будет делать Object.assign(this._state, payload) в мутабельном стиле (для оптимизации изменения глубоко вложенных структур придется чуть усложнить). Ну и switch-case туда. При этом рендеринг UI компонентов будет оптимизирован из коробки - что бы мы ни меняли, будут точечно ререндериться только те компоненты, которые читают конкретный параметр, и только когда он изменился.

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

То есть никто не запрещает написать на Mobx флоу работы с данными как в Redux, описать это в тех. документации и на ревью проверять, что все разработчики придерживаются этого флоу. Либо не Redux-like, а выбрать удобный для проекта и команды способ установки-получения значений, ведь этих паттернов огромное количество, и Mobx с очень большой вероятностью сможет так работать. То есть он - это все остальные СТМ вместе взятые, остается только выбрать удобный подход и консистентно работать с ним.

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

Ну так он перерндерился только если данные были изменены. Что за странная "сложность понять"? Ну читает у вас компонент 50 реактивных полей, одно изменилось - он перерендерился. С чего вообще вы вдруг решили объявить рендер компонента проблемой?)) Это нормально. Более того, я не могу представить задачу даже с помощью натягивания совы на глобус, где проблема была бы именно в MobX, а не в вашем компоненте. Т.е. "задача" понять при изменении какого именно поля произошел перерендер изначально не имеет никакого смысла практического. Ну а если так сильно хочется, то и у MobX есть dev tools. Но я dev tools мобикс вообще не пользуюсь уже как 8 лет работая с ним каждый день.

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

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

Сложно сравнить хорошие продукты, которыми являются и mobX и его конкуренты, поэтому я и говорю о крайних случаях, где одни чуть лучче других, по моему опыту.

Все стейт менеджеры хороши, и холивар тут как бы нет причин начинать

Хороши это конечно хорошо - но что удобнее в работе, производительнее и проч.

Лично я по отзывам не стал связываться с Redux - и сразу (после небольшого research здесь) начал писать свои первые приложения React с использованием Mobx. И в общем-то на мой, ни разу не спеца в этой сфере, взгляд - получалось достаточно удобно (не спрашивайте почему - это на уровне ощущений, тем более давно прошедших, практически в до ChatGPT-ую эпоху).

Мне в этом многообразии State Manager-ов для React кажется какой-то перебор (глядя из мира Java - где практически один стандарт для разработки Enterprise backend-a - Spring и 2-х систем сборки (Maven + Gradle)). Тут же - для SM 1000 либ. Это перебор КМВ. Тем более что есть жалобы от разработчиков на SM от создателей React на сложность и многословность (что первая, что вторая их версия) - вроде же Mobx должен решать проблемы, ну Zustand как вариант, да хотя бы тот же Redux.

Просто интересно, что это за тема такая горячая (или перегретая) с SM в React?

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

Если серьезно, то по сути реальная разница есть между либами, которые мутируют объекты (mobx, valtio, ...) и теми, кто идёт по пути реакта и объекты не мутирует.

А в рамках одной группы разница не так велика. Я, наверное, соберу ещё хейта за такие мысли, но разница буквально в предпочтительных паттернах и синтаксисе.

Если прям интересно, очень рекомендую книгу "Micro state Management react hooks". Читается за день, даёт ответы на большинство вопросов связанных с см в контексте react приложений.

по сути реальная разница

Вот щас понятно, благодарю

разница буквально в предпочтительных паттернах и синтаксисе

Да, mobx об этом пишет на главной тоже -  MobX brings transparent functional reactive programming (TFRP, a concept which is further explained in the MobX book)

очень рекомендую книгу "Micro state Management react hooks"

Зачитаю, спасибо

А чего про solid и recoil не написал? Без них список не полный как-то

А тут все просто, не использовал их еще)

Репозиторий Recoil уже больше года как заархивирован.

Solid - это Solid.js? Так это и не стейт менеджер.

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

думаю ожидали фрейморк который нельзя называть и который даже лучше мобикса ?)

Делаю, что могу)))

Знал, на что я шел

в обзоре упущены reatom и effector

Так они оба намного хуже даже чем redux, поэтому их упоминать даже смысла нет. А уж до MobX им вообще не дотянуться никогда. Они изначально спроектированы чтобы писать говнокод.

А чем хуже? По вашему мнению. Я с ними не работал, экспертизы тут не имею

Сравните наглядность и чистоту кода и это вообще на hello world, в реальности MobX уходит в огромный отрыв по чистоте, наглядности и удобству написания кода:

MobX:

Effector:

Reatom:

А теперь более реальный пример:
MobX:

// stores/appStore.ts
import { makeAutoObservable } from 'mobx';

class AppStore {
  title = '';
  items: string[] = [];
  isItemsFetching = false;

  constructor() {
    makeAutoObservable(this);
  }

  handleTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
    this.title = e.target.value;
  };

  fetchItems = async () => {
    this.isItemsFetching = true;
    try {
      // Имитация API-запроса
      const response = await fetch('/api/items');
      this.items = await response.json(); // предположим, что возвращается string[]
    } catch (error) {
      console.error('Failed to fetch items', error);
      this.items = [];
    } finally {
      this.isItemsFetching = false;
    }
  };
}

export const appStore = new AppStore();

Effector:

// stores/appStore.ts
import { createEvent, createEffect, createStore, sample } from 'effector';

// События
export const titleChanged = createEvent<string>();
export const fetchItems = createEvent();

// Эффект (асинхронная операция)
export const fetchItemsFx = createEffect(async (): Promise<string[]> => {
  const res = await fetch('/api/items');
  return res.json();
});

// Состояния
export const $title = createStore<string>('');
export const $items = createStore<string[]>([]);
export const $isItemsFetching = fetchItemsFx.pending;

// Обновление title
$title.on(titleChanged, (_, newTitle) => newTitle);

// Запуск эффекта при вызове fetchItems
sample({
  clock: fetchItems,
  target: fetchItemsFx,
});

// Обновление items после успешного завершения
$items.on(fetchItemsFx.doneData, (_, items) => items);
// Ошибку можно обработать через fetchItemsFx.fail
// hooks/useAppStore.ts
import { useUnit } from 'effector-react';
import {
  $title,
  $items,
  $isItemsFetching,
  titleChanged,
  fetchItems,
} from '../stores/appStore';

export const useAppStore = () => {
  const [
    title,
    items,
    isItemsFetching,
    setTitle,
    triggerFetch,
  ] = useUnit([
    $title,
    $items,
    $isItemsFetching,
    titleChanged,
    fetchItems,
  ]);

  const handleTitle = (e: React.ChangeEvent<HTMLInputElement>) =>
    setTitle(e.target.value);

  return {
    title,
    items,
    isItemsFetching,
    handleTitle,
    fetchItems: triggerFetch,
  };
};

Reatom:

// stores/appStore.ts
import { atom, action, withDataAtom } from '@reatom/framework';

// Простые атомы
export const titleAtom = atom('', 'title');
export const itemsAtom = atom<string[]>([], 'items');
export const isItemsFetchingAtom = atom(false, 'isItemsFetching');

// Действие для изменения title
export const handleTitle = action('handleTitle', (ctx, value: string) => {
  titleAtom(ctx, value);
});

// Действие с побочным эффектом
export const fetchItems = action('fetchItems', async (ctx) => {
  isItemsFetchingAtom(ctx, true);
  try {
    const res = await fetch('/api/items');
    const items = await res.json();
    itemsAtom(ctx, items);
  } catch (error) {
    console.error('Failed to fetch items', error);
    itemsAtom(ctx, []);
  } finally {
    isItemsFetchingAtom(ctx, false);
  }
});
// hooks/useAppStore.ts
import { useAtom } from '@reatom/react';
import {
  titleAtom,
  itemsAtom,
  isItemsFetchingAtom,
  handleTitle,
  fetchItems,
} from '../stores/appStore';

export const useAppStore = () => {
  const [title, setTitle] = useAtom(titleAtom);
  const [items] = useAtom(itemsAtom);
  const [isItemsFetching] = useAtom(isItemsFetchingAtom);

  const handleTitleWrapper = (e: React.ChangeEvent<HTMLInputElement>) =>
    handleTitle(e.target.value);

  return {
    title,
    items,
    isItemsFetching,
    handleTitle: handleTitleWrapper,
    fetchItems,
  };
};

даже в самом первом элементарном примере код мобх раздувается на десяток строк, необходимость создавать классы и писать конструктор с makeAutoObservable. В а реатоме для модели достаточно три строки для всего того же. Ну и в реатоме есть аналог observer — reatomComponent. А еще можно взять последнюю версию реатома а не v3, где больше нет явных контекстов (ctx). В рамках первого примера хуже всего выглядит эффектор, а потом уже мобх

в следующем примере вижу ошметки withDataAtom — похоже ты решил скрыть то, как реатом работает с асинхронностью на самом деле (где не нужно создавать пендинг состояния, делать try catch и так далее) и уровнять это до того, что может дать мобх из коробки. Не хорошо. Что там мобх может дать для работы с асинхронностью из коробки? Может быть еще и саспенс нативно поддерживает? В примере mobx описана модель, но нет getAppStore, создается впечатление что варианты реатома и эффектора просто раздули лишним кодом, раз для мобх он не нужен.

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

думайте

 В примере mobx описана модель, но нет getAppStore

Так там же стор уже создан и просто работает сразу в компоненте.

export const appStore = new AppStore();
import { appStore } from 'store/appStore';

const MyComponent = observer(() => {
    return (
      <div>
         <div>count: {appStore.count}</div>
         <button onClick={appStore.incr}>incr</button>
         <button onClick={appStore.decr}>decr</button>
      </div>
})

создается впечатление что варианты реатома и эффектора просто раздули лишним кодом, раз для мобх он не нужен.

Ну для MobX он и правда не нужен. И да, для reatom и effector увы нужен, и чем более реальным будет пример, тем более громоздкими и неудобными будут reatom и effector.

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

Я в 2018 году написал роутер который использует mobx и использую его по сей день в проектах, так же в 2018 году написал микролибу для форм и валидации которая использует MobX и так же юзаю по сей день во всех проектах. Времени на написание этих вещей ушло ну 2-3 дня от силы, это с учетом дальнейший модернизаций по мере надобности. Ну и так далее, любая вещь которая мне упростит жизнь - я просто беру и пишу её, это вообще элементарно. Хочешь кэш? Легко, хочешь debiunce? Легко. Ну кстати всё это тоже реализовано у меня например 8 лет назад)

Ну и в реатоме есть аналог observer

Если доводить до идеала reatom/effector/и т.п., то в итоге получится MobX.

Так там же стор уже создан и просто работает сразу в компоненте.

Ну так у реатома и эффектора тоже сторы уже созданы... Подмечу, что в варианте с useAppStore хуки создают подписки сразу на все части стора, когда мобх тогда тоже должен иметь геттер чтобы сделать подписки сразу на все части его стора — это уже не валидный пример. Нужно уже что-то одно выбрать: либо биндинги происходят вручную по мере надобности стора, либо все целиком, иначе это нечестно. И да, реатом может без хуков тоже обойтись (reatomComponent), во втором примере реатом и мобх друг друга копируют, если переписать пример с реатомом правильно.

Если вопрос состоит в доменном объединении частей стора — можно просто воспользоваться namespace экспортами (не TSными, а нативными через звёздочку)

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

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

Если доводить до идеала reatom/effector/и т.п., то в итоге получится MobX.

никогда, потому что mobx это прокси/геттер реактивность на всегда и это его проклятие. И да, это наоборот мобх сейчас в положении, когда он не достигает до идеала экосистему реатома и эффектора

Че-то всё куда-то в сторону и мимо. Причем тут честно не честно? Когда пишешь на MobX то код такой, когда пишешь на effector сякой, когда пишешь на reatom третий. Все, тут без вариантов. Cделайте самый лучший аналог, напишите на reatom или effector вот этого:

https://stackblitz.com/edit/stackblitz-starters-eq7zm5dw?file=src%2FApp.tsx

import { makeAutoObservable } from 'mobx';

const sleep = (ms = 1000) => new Promise((res) => setTimeout(res, ms));

class AppStore {
  title = '';
  counter = 0;
  items: string[] = [];
  isItemsFetching = false;

  constructor() {
    makeAutoObservable(this);
  }

  handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    this.title = e.target.value;
  };

  fetchItems = async () => {
    this.isItemsFetching = true;
    try {
      // Имитация API-запроса
      await sleep(1000);
      this.items = ['hello', 'world'];
    } catch (error) {
      console.error('Failed to fetch items', error);
      this.items = [];
    } finally {
      this.isItemsFetching = false;
    }
  };

  incr = () => {
    this.counter++;
  };

  decr = () => {
    this.counter--;
  };
}

export const appStore = new AppStore();
export const App = observer(() => {
  return (
    <div>
      <h1>Hello World: {appStore.title}</h1>
      <div>Counter: {appStore.counter}</div>
      <div>
        <input value={appStore.title} onChange={appStore.handleTitleChange} />
      </div>
      <div>
        <button onClick={appStore.incr}>incr</button>
        <button onClick={appStore.decr}>decr</button>
        <button onClick={appStore.fetchItems}>fetch items</button>
      </div>
      {appStore.isItemsFetching && <div>fetching data...</div>}
      <div>items: {appStore.items.join(',')}</div>
    </div>
  );
});

И выложите на stackblitz, можете просто этот пример редактировать.

https://stackblitz.com/edit/stackblitz-starters-eq7zm5dw?file=src%2FApp.tsx

import './style.css';
import {
  action,
  atom,
  computed,
  reatomNumber,
  sleep,
  withAsyncData,
} from '@reatom/core';
import { reatomComponent } from '@reatom/react';

const counter = reatomNumber(0);

const title = atom('');
const handleTitleChange = action((e: React.ChangeEvent<HTMLInputElement>) => {
  title.set(e.target.value);
});

const fetchItems = computed(async () => {
  await sleep(1000);
  return ['hello', 'world'];
}).extend(withAsyncData({ initState: [] }));
export const App = reatomComponent(() => (
  <div>
    <h1>Hello World: {title()}</h1>
    <div>Counter: {counter()}</div>
    <div>
      <input value={title()} onChange={handleTitleChange} />
    </div>
    <div>
      <button onClick={() => counter.increment()}>incr</button>
      <button onClick={() => counter.decrement()}>decr</button>
    </div>
    {!!fetchItems.pending() && <div>fetching data...</div>}
    <div>items: {fetchItems.data().join(',')}</div>
  </div>
));

https://stackblitz.com/edit/stackblitz-starters-67ebpirl?file=src%2FReatom.tsx пожалуйста, только я тут упростил чтобы был ленивый запрос при монтировании компонента, надеюсь на мобксе это тоже можно в двух строчках кода сделать, или даже заменой двух операторов

Не знаю что вы там сделали, но как минимум это не работает

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

Нет уж, делай чтобы именно по нажатию на кнопку было. Что бы сравнение было 1 к 1, а не 1 к 0.5

блин окей, не думал что это на столько принципиально. Я вернул кнопку как была и заменил на 12 строке computed -> action, чтобы было понятно чего достаточно для этого. Жду кстати встречного диффа от мобх где тоже есть ленивый фетчинг, а то запросы такого рода почти никогда не грузятся по нажатию кнопки, сам знаешь.

Касаемо ошибки: почему-то возникла именно в мобх сниппете, мб что-то сломал я. Экспортнул свой и работает.

Все чекать по той же ссылке

Жду кстати встречного диффа от мобх где тоже есть ленивый фетчинг

Вот https://stackblitz.com/edit/stackblitz-starters-dzte4xgd?file=src%2FApp.tsx

onBecomeObserved(this, 'items', this.fetchItems);

Все чекать по той же ссылке

Ну вот теперь можно посмотреть какой код легче для восприятия, в reatom я вижу кучу специфичных ключевых слов action, atom, reatomNumber, withAsyncData, reatomComponent

В MobX в 99.9% кейсов нужно только 2 - makeAutoObservable и observer, всё остальное простой нативный js код)

Ещё в reatom:

<button onClick={() => counter.increment()}>incr</button>
<button onClick={() => counter.decrement()}>decr</button>

Каждый раз в компоненте нужно создавать новую стрелочную функцию
А в MobX все просто работает по ссылке

<button onClick={appStore.incr}>incr</button>
<button onClick={appStore.decr}>decr</button>

Ещё вот это вот, все в виде функций, т.е. сразу не понятно это вообще функция или что

А в MobX всё проще в это плане и яснее сразу

Вот https://stackblitz.com/edit/stackblitz-starters-dzte4xgd?file=src%2FApp.tsx

при первом монтировании не меняется пендинг состояние... как ты это объяснишь? там же fetchItems вызывается

Ну вот теперь можно посмотреть какой код легче для восприятия, в reatom я вижу кучу специфичных ключевых слов action, atom, reatomNumber, withAsyncData, reatomComponent

1) action и atom понятно зачем нужны,
2) withAsyncData это экстеншен, который дает хендлинг асинхронных состояний (pending, data, error) и конкурентность (отмена предыдущих запросов),
3) reatomComponent это вообще observer из моих камон, зачем это вообще упоминать? Правильно, если слово не из мобх, значит специфичное ключевое слово и значит это аргумент в пользу мобх. Или все же нет?
4) reatomNumber как раз чтобы не бойлерплейтить всякие каунтеры, стрелочные функции там только потому что на вход принимается параметр ступени инкремента (чтобы не только на единицу увеличивать), у этого нет каких-то особенных причин: можно такие же экшены сделать и без аргументов и получить такие же стабильные ссылки, только это на столько не важно на самом деле, учитывая что это синтетический пример который не сложнее тудулиста

я вижу кучу специфичных ключевых слов action, atom, reatomNumber, withAsyncData, reatomComponent

То есть, аргумент сводится только к тому, что тут пару незнакомых слов для знакомящегося с либой? Можешь объяснить, как работает этот аргумент и почему это вообще плохо, если буквально любая либа имеет свои примитивы и в любую либу нужно вникать, разбираться?

Ещё вот это вот, все в виде функций, т.е. сразу не понятно это вообще функция или что

предположу, что если после идентификатора стоит function call operator, то наверное логично, что это функция??? этот аргумент ни на что не действует

если буквально любая либа имеет свои примитивы и в любую либу нужно вникать, разбираться?

Спорное утверждение, вот ваш переписанный пример на неизвестной вам «либе»:

const state = makeObservable({
  title: '',
  count: 0,
  data: [],
  pending: false,
  fetchItems() {
    this.pending = true;
    sleep().then(() => {
      this.data = ['hello', 'world'];
      this.pending = false;
    });
  },
  increment() {
    ++this.count;
  },
  decrement() {
    --this.count;
  },
  onChange(event) {
    this.title = event.target.value;
  },
});

export const App = observer(() => (
  <div>
    <h1>Hello World: {state.title}</h1>
    <div>Counter: {state.count}</div>
    <div>
      <input value={state.title} onChange={state.onChange} />
    </div>
    <div>
      <button onClick={state.increment}>incr</button>
      <button onClick={state.decrement}>decr</button>
      <button onClick={state.fetchItems}>fetch items</button>
    </div>
    {!!state.pending && <div>fetching data...</div>}
    <div>items: {state.data.join(',')}</div>
  </div>
));

Как видно, используется всего две функции makeObservable и observer. Во что тут нужно вникать? С чем тут нужно разбираться? Потыкать вживую можно в песочнице.

в самом первом элементарном примере код мобх раздувается на десяток строк, необходимость создавать классы

Я понял, что вы не любите классы, и мне совершенно непонятно почему. Вот этот же пример с этой же либой, только с синтаксисом классов:

class State extends Observable {
  title = '';
  count = 0;
  data = [];
  pending = false;

  async fetchItems() {
    this.pending = true;
    await sleep();
    this.data = ['hello', 'world'];
    this.pending = false;
  }

  increment() {
    ++this.count;
  }

  decrement() {
    --this.count;
  }

  onChange(event) {
    this.title = event.target.value;
  }
}

export const App = observer(({ state }: { state: State }) => (
  // разметка
));

// ...
<App state={new State} />     

Как видите, запись не менее лаконичная, но дополнительно мы еще и нормальную типизацию получили без лишней строчки кода.

при первом монтировании не меняется пендинг состояние... как ты это объяснишь? там же fetchItems вызывается

Да, это бажок, но в целом для меня он не имеет значения, я таким образом все равно никогда ничего не делаю. Я вызываю fetch'и либо в конструкторе класса, либо в "конструкторе" компонента(useState(() => { someState.fetchItems(); }))

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

К тому, что KISS(Keep It Simple Stupid) превыше всего. Если можно что-то сделать проще - нужно обязательно это сделать проще. Если разработка реального приложения требует всего 2х функций: makeAutoObservable и observer, то это куда более предпочтительней.

Можешь объяснить, как работает этот аргумент и почему это вообще плохо, если буквально любая либа имеет свои примитивы и в любую либу нужно вникать, разбираться?

Вникать и разбираться - понятие растяжимое, в каких-то либах достаточно 15-60 минут, в каких-то десятки часов, десятки дней, месяцы. Опять же, я отдаю предпочтения вариантам которые 99.9% потребностей реальных приложений закрывают "изучением" 2х функций (15 минут времени), а именно makeAutoObservable и observer. Для остальных 0.099% случаев придется ещё потратить 15 минут, чтобы "изучить" ещё 2 функции: reaction и autorun. Ну а для всего остального (0.001%) уже всё остальное, тут так и быть придется ещё часок другой времени потратить.

предположу, что если после идентификатора стоит function call operator, то наверное логично, что это функция??? этот аргумент ни на что не действует

Логично, что например title это просто строка/переменная, а не функция. И console.log(title) должен вывести название, а не функцию(как в reatom).Получается привычные и интуитивно понятные вещи на ровном месте переворачиваются вверх дном при использовании reatom. Зачем это всё, когда можно просто писать код используя нативный JS?

Да, это бажок, но в целом для меня он не имеет значения

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

Если разработка реального приложения требует всего 2х функций: makeAutoObservable и observer, то это куда более предпочтительней.

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

Опять же, я отдаю предпочтения вариантам которые 99.9% потребностей реальных приложений закрывают "изучением" 2х функций

ну то есть ты действительно думаешь что 99.9% потребностей реальных приложений закрывают обычные стейт контейнеры с компьютедами и экшенами и способ подписываться на них вне правил хуков? Наверное это просто про твой опыт, я не буду с этим спорить

Логично, что например title это просто строка/переменная, а не функция.

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

теперь касаемо примера на мобх:
1) никто в здравом уме не будет писать вручную try catch, мутацию пендинг состояний для кучи асихронщины, никто не будет руками реализовывать конкурентность если она понадобится. Как это решается, через кучу обвязок, их композицию? Проектные знания, да?
2) на полном серьезе вызывается fetchItems вызывается при монтировании, но изменение его пендинг состояния не отражается в UI, он просто неактуальное. Как это вообще возможно в топ1 либе для реактивности? У меня нет идей, мне очень интересно узнать из-за чего это
3) для простых атомических несвязных состояний тоже придется класс создават, makeAutoObservable делать? Это же точка появления бойлерплейта на ровном месте. А в реатоме можно создавать фабрики точно так же как сторы классами, но без классов и без пометки что это реактивная сущность, только это делается по мере необходимости

никто в здравом уме не будет писать вручную try catch, мутацию пендинг состояний для кучи асихронщины

Ну, у меня это автоматизировано, и да, все так же с использованием MobX и написанного плагина для Vite. У асинхронных методов автоматически появляются реактивные свойства .fetching / .error / .callsCount. Я пользуюсь этим последние 4 года. Но это уже всё другой уровень. До этого писал this.fetching = true и т.п. руками, вообще без проблем, зато всё сверх наглядно и очевидно.

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

Тоже реализовал 1 раз лет 8 назад в виде одной функции и забыл, сейчас тоже автоматом через плагин это проставляется, если указан декторатор нужный, и отмена запросов так же автоматом реализовывается. И debounce и вообще любые "неудобства" я решил ещё много много лет назад, поэтому я их даже в расчет не беру, умение их решать это базовый навык разработчика любого.

Вы как-то не совсем правильно сравниваете Mobx и Reatom.

Mobx - шуруповерт без насадок. Он добавляет в JS реактивность для объектов, то есть если меняется свойство - то вызываются подписчики, все. Это хороший мотор, который может крутить все что угодно. Это не СТМ, а очень крутой полифилл для deprecated нативного Object.observe. Если нужны "насадки" (экосистема) - этим занимаются другие библиотеки. Можно писать обычный код на js с обычными объектами/классами, но только реактовые компоненты не будут ререндериться. Добавляем observable+observer - все точечно ререндерится.

Reatom - это большая коробка отверток "на все случаи жизни". Как вы заметили - туда запихнули работу с асинхронностью, роутер, ленивый фетчинг, кешы и механизмы для работы с апи, снепшоты состояний и даже sleep функцию. Это можно сказать целый фреймворк со своими гайдлайнами и способами написания кода (например, что свойства надо вызывать как функции counter()). То есть нужно изначально писать на Reatom весь код, нельзя написать на js-объектах и обернуть в пару функций, чтобы все стало реактивным.

Странно слышать, что в Mobx "нет этого или того-то", у него абсолютно другая парадигма - "делать свое дело и делать хорошо". А у ряда библиотек, например Реакта, другая парадигма - "сделаем все-в-одном". Вместо того, чтобы просто рендерить компоненты в DOM и делать это хорошо, они стали внедрять свой стейт-менеджмент, асинхронные механизмы, серверные компоненты, навязывать парадигмы вроде иммутабельности и правил хуков. Да и у ряда компаний есть такой подход - Яндекс например или Сбер стараются охватить все рынки, от образования и развлечений до доставки еды, платежных систем и такси.

Кому-то нравится, когда все в единой экосистеме и "поддерживается одной командой", пусть определенные части сделаны откровенно ужасно (мы ведь тут в статье про Реакт обсуждаем СТМ для него, а ведь у него уже есть встроенная СТМ, просто она слишком плоха). А кто-то выбирает пользоваться специализированными сервисами/библиотеками с ясной ответственностью, которые сделаны максимально качественно. И чем больше насмотренности, экспертизы и опыта, тем менее хочется мириться с подходом "все-в-одном", и вы тут с нами в одной лодке, отказавшись от реактовых подходов. Но в Реатоме я как и другие вижу много недостатков, и не готов полностью на него переходить, а выберу лучше несколько специализированных библиотек.

Спасибо за комментарий - обсудил его с LLM - очень интересная получается картина.. Может статью напишите, такое ощущение вам есть что сказать по данной теме :)

Идею уже выразил в этом комментарии, а разбавлять ее водой в статье не вижу смысла... Всегда на этапе вхождения в программирование лучше начинать с комплексных фреймворков, где все согласовано между собой и продумано. А когда начинаешь ловить себя на мысли "здесь и здесь можно сделать лучше, но внедрить это во фреймворк можно только костылями" - то открывается мир "сборки по кирпичикам". И оказывается, что в сообществе программистов огромное количество гранулярных библиотек, которые хорошо делают ясно описанный набор задач, остается только удобно их связать. Либо предложить свое видение миру в виде такой гранулярной библиотеки - для этого не нужна огромная команда и финансирование, а достаточно времени по вечерам.

Самое главное - давать удобные "коннекторы" для других "кирпичиков". В случае Mobx даже их не требуется - синтаксис остается идентичным, как если Mobx вообще не подключать, а писать на голом JS. То есть по умолчанию с ним совместимо большинство js-библиотек, если конечно у них не встроена своя система состояний.

потому что mobx это прокси/геттер реактивность на всегда и это его проклятие

Эмм, а чем собственно проклятье? В том, что оно просто работает прекрасно и всё? Просто пишешь код и оно работает как ожидается. В этом проклятье?

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

Кстати, а логгеры состояний есть в мобх, чтобы можно было видеть каскад изменений (то есть причинность изменений или стектрейсы изменений), девтулзы может даже?

особенно когда теряешь реактивность от неконвенциональных действий

Так вот же, потеря реактивности в reatom

Не написал title() и counter(), и всё)

Кстати, а логгеры состояний есть в мобх, чтобы можно было видеть каскад изменений (то есть причинность изменений или стектрейсы изменений), девтулзы может даже?

Есть MobX DevTools, но в 99.9% это вообще не нужно, я вообще например этим 8 лет не пользуюсь

Не написал title() и counter(), и всё)

отлично, и код не прошел в CI из-за ошибок типов. Ну не нужно же вот так пошло это делать, код же подчеркивается красным. Чего мобх никогда не подскажет... ну, потому что KISS, использование языка и так далее, чтобы скрывать реактивную семантику

Есть MobX DevTools, но в 99.9% это вообще не нужно, я вообще например этим 8 лет не пользуюсь

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

Логгеры есть (девтулзы), но зачем они нужны, когда можно по стектрейсу встроенных девтулзов в сам браузер найти причину изменений, а некоторые браузеры даже группируют события в стектрейсе по MobX (на скрине zen браузер)

Я просто оставлю это здесь:

export class AppStore extends $mol_object {

    @ $mol_mem
	static title( next = '' ) { return next }

	@ $mem_mem
	static items() {
		return this.$.$mol_fetch.json( '/api/items' ).json()
	}

}
Sign up to leave a comment.

Articles