Pull to refresh

Comments 44

Сложилось впечатление, что девиз эффектора «Я могу делать то же самое, только сложнее». Видно я не модный и не молодёжный :)
При грамотно созданных моделях в компоненте не нужно страдать и отслеживать все свои телодвижения по обновлению хранилища. Подключили его в компонент — он всегда актуален и перерисовывается при каждом обновлении хранилища. Никаких тебе Mobx-овых action, @computed и прочей ручной настройки. Каеф :)

Скиньте пожалуйста на codesandbox пример с грамотно настроенным Effector'ом чтобы сравнить это с MobX на деле, и на реальном коде, а не на словах.

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

Из компонента посылаем запрос на бек (потому что нужно отслеживать статус запроса).

Здесь же получили данные и положили их в хранилище.

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

Effector позволяет реализовать работу напрямую: из хранилища послал запрос, в хранилище положил ответ. И наоборот.

Что простите?? =)) А MobX не позволяет??))))
Я так понимаю, имелось ввиду, что, если делать это с помощью MobX, нужно будет в компоненте в сomponentDidMount/useEffect вызывать функцию для запроса на сервер
компонент используется как прокси в этом случае. И это кажется очень странным


из хранилища послал запрос

а вот это кажется логичным? серьезно?
  // Не нужно использовать пропс key, как было с map()
  const users = useList($users, (user) => (

Магия какая-то. Как это не нужен key?! Полез в код:


  return Array.from({length}, (_, i) =>
    React.createElement(Item, {
      index: i,
      key: i,

Они туда просто воткнули индекс массива. Т.е. ваш код с key={user.id} выглядит куда лучше.


А ещё там внутри какая-то обёртка:


const Item = React.useMemo(() => {
    const Item = withDisplayName(
      `${list.shortName || 'Unknown'}.Item`,
      ({index, keys}: {index: number; keys: any[]}) => {
        const item = useStoreMap({
          store: list,
          keys: [index, ...keys],
          fn: (list, keys) => list[keys[0]],
        })
        return fnRef.current(item, index)
      },
    )
    return React.memo(Item)
  }, [list])

Если я правильно это понял, то тут пересоздаётся класс при изменении list. Т.е. полный unmount и последующий mount всего dom-под-древа. Ух. Видимо предполагается что list высечен в камне… Но смотрим уже ваш код:


  // Возвращаем измененный стейт
  return [...state];

Таки не высечен.


В целом в коде либы я вижу много any. И кажется автору effector-а нужно купить новую клавиатуру, т.к. похоже у него сломалась кнопка enter (весь код слипся).


const updateStore = (state: IUser[], data: IUser) => {
  const userIndex = state.findIndex((user) => user.id === data.id);

  // Изменяем стейт
  if (userIndex > -1) {
    state.splice(userIndex, 1, data);
  } else {
    state.push(data);
  }

  // Возвращаем измененный стейт
  return [...state];
};

Я не очень понял — зачем вы с одной стороны мутируете стейт, с другой стороны играете в иммутабельность. Это имеет какой-то смысл? :)

Привет) Там в коде useList, list это инстанс стора, который обычно статично создается, а внутри него уже стейт меняется

А по поводу индекса в ключе согласен, useList лучше не использовать для списков элементы которого могут удаляться
лучше не использовать для списков элементы которого могут удаляться

Привет, не ожидал тебя тут встретить ;)
Не только удаляться, но и:


  • менять свою сортировку
  • или когда элемент может быть добавлен в начало или в середину

В общем любая ситуация, когда индекс у элемента может измениться\сместиться.


Там в коде useList, list это инстанс стора, который обычно статично создается, а внутри него уже стейт меняется

Ну если ссылочная целостность гарантируется — то проблемы нет. До тех пор пока кто-нибудь не начнёт жонглировать разными store-ми. В этом случае начнётся шапито.

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

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


Никаких тебе Mobx-овых action, @computed и прочей ручной настройки. Каеф :)

На Mobx тоже можно писать в стиле "big blob of code" и безоговорочно верить в автоматику. makeAutoObservable есть, в конце концов. Правда, в конце такого отважного пути обычно выясняется, что чисто случайно кто-то где-то что-то не оттуда и не так дёргает, и в итоге браузер крутит в сотни раз больше яваскрипта, чем это реально нужно для выполнения задач. Но юзер всё стерпит.


А в нормальном мире всё это аннотирование помогает структурированности и читаемости кода, даже если потенциально можно вместо него автоматику подключить.


Вообще статья напоминает классические истории по неискание лёгких путей. Зачем что-то писать кратко, когда можно написать длинно? Зачем использовать typescript-ready решения, когда можно самим нафигачить типы? Зачем нормально использовать FRP-либу, когда можно взять другую FRP-либу, и рассказать всем, что вот с ней-то всё будет ух и вжжж?

Перечитайте, плиз, начало статьи.

ATTENTION: приведенные в статье размышления являются сугубо субъективными, поэтому ваше мнение может отличаться от моего. Они носят обозревательный характер и позволяют познакомиться с Effector на моем примере.

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

Переехали с Redux на Effector, boilerplate code в проекте значительно уменьшился. Спасибо за статью!
Открою секрет, есть MobX, вот там по-настоящему код уменьшится и будет гораздо красивее и понятнее этой лапши.
Спасибо, я всегда рад секретам)

Но с mobX не приходилось работать. По тому что я смотрел очень быстро — он тоже значительно больше, чем effector. Создание классов зачем то. Мне кажется, и готов даже проверить, что любой функционал на effector будет описан более быстро.

Ну и так же плюс эффектора — русские разработчики создатели, которые могут помогать, слушать и слышать
По тому что я смотрел очень быстро

Ясно…
он тоже значительно больше, чем effector

Ясно…
Создание классов зачем то

Ясно…
Ну и так же плюс эффектора — русские разработчики создатели, которые могут помогать, слушать и слышать

Ясно…

«Серьезные аргументы» однако

Мне кажется, и готов даже проверить, что любой функционал на effector будет описан более быстро.

Просто напишите аналог и сравните.
class SomeState {
    @observable.ref items: ISomeItem[] = [];
    @observable error: string = null;
    @observable isFetching = false;
    
    constructor() {
        this.fetchData();
    }

    fetchData = async () => {
        this.isFetching = true;

        try {
            this.items = await apiGetSomeItems();
            this.error = null;
        } catch (e) {
            this.error = e.message;
        } finally {
            this.isFetching = false;
        }
    }
}

const SomeItemsPage = observer(() => {
    const [state] = useState(() => new SomeState());
    
    if (state.isFetching) return <div>Fetching data...</div>;
    if (items.error) return <div>Error: {items.error}</div>;
    
    return (
        <div>
            {state.items.map((item) => <div>{item.title}</div>)}
        </div>
    );
})


А если хочется совсем мало кода, то просто вот так
const SomeItemsPage = observer(() => {
    const [items] = useState(() => new WithApiRequest(getApiData));

    if (items.fetching) return <div>Fetching data...</div>;
    if (items.error) return <div>Error: {items.error}</div>;

    return (
        <div>
            {items.data.map((item) => <div>{item.title}</div>)}
        </div>
    );
})

Где WithApiRequest
class WithApiRequest<T> {
    @observable.ref data: T = null;
    @observable error: string = null;
    @observable fetching = false;

    dataFetcher: () => Promise<T> = null;

    constructor(dataFetcher: () => Promise<T>, fetchOnConstruct = true) {
        this.dataFetcher = dataFetcher;
        if (fetchOnConstruct) {
            this.fetchData();
        }
    }

    fetchData = async () => {
        this.fetching = true;

        try {
            this.data = await this.dataFetcher();
            this.error = null;
        } catch (e) {
            console.error(e);
            this.error = e.message;
        } finally {
            this.fetching = false;
        }
    }
}
Хорошо. Вот написанное выше на effector

export const getSomeItemsFx = createEffect(apiGetSomeItems)

export const $someItems = createStore<ISomeItem[]>([])
  .on(getSomeItemsFx.doneData, (state, items) => items);

export const $error = createStore<string | null>(null)
  .on(getSomeItemsFx.fail, (state, { error }) => error.message)
  .reset(getSomeItemsFx);


export const WithRequest = () => {
  const isLoading = useStore(getSomeItemsFx.pending);
  const someItems = useStore($someItems);
  const error = useStore($error);

  useEffect(() => {
    getSomeItemsFx()
  }, [])

  if (isLoading) return <div>Fetching data ...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      {someItems.map((item) => <div key={item.id}>{item.title}</div>)}
    </div>  );
};


Код компонента фактически идентичен, а код модели существенно меньше. Плюс сам запрос начнётся после монтирования компонента, а не при его маунте

Это не эквивалентный код. У вас одно состояние на все инстансы компонента, в то время как код MobX выше — будет иметь одно независимое состояние на один инстанс компонента.

Если вы хотите, оберните в замыкание

const createSomeItemsApi = () => {
  const getSomeItemsFx = createEffect(apiGetSomeItems)

  const $someItems = createStore<ISomeItem[]>([])
    .on(getSomeItemsFx.doneData, (state, items) => items);

  const $error = createStore<string | null>(null)
    .on(getSomeItemsFx.fail, (state, { error }) => error.message)
    .reset(getSomeItemsFx);

  return {
    getSomeItemsFx,
    $someItems,
    error
  }
}


Так что да… Ясно… Ведь не факт что требуется разное состояние. Быть может нужно одно состояние в разных местах

Вы замечаете, как код перестаёт быть "сильно короче" варианта с MobX, стоит вам только начать его приближать по функционалу?
Добавить еще некоторые до сих пор отличающиеся моменты (мелочи) — и вообще будет мало разницы.


Так что да… Ясно… Ведь не факт что требуется разное состояние. Быть может нужно одно состояние в разных местах

Это абсолютно ортогонально обсуждаемому здесь моменту "чей код длиннее".

Хотел бы еще добавить по моменту сложности. В mobx версии все топорно и просто, есть свойства класса — есть функция фетчинга, которая записывает результат в эти свойства. В эффекторе добавляются вызовы функций createEffect, createStore, 2 стрим-пайплайна, завязанные на специфические свойства (.doneData, .fail), используется 3 импорта (getSomeItemsFx, $someItem, $error) вместо одного, и для них добавляются специфические константы (вместо обычных названий у свойств в классе), а также 2 дополнительных константы в самом компоненте вместе с двумя дополнительными хуками. Вместо стандартных джаваскриптовых try-catch-finally используются методы эффектора, которые требуется изучить. Структура стора размазывается по 3 сущностям, и требуется изучить код, чтобы понять их взаимосвязь, в то время как в mobx версии свойства класса видно с первого взгляда, и легко найти, где они изменяются, с помощью IDE (findUsages). Функция getSomeItemsFx не содержит полную логику взаимодействия, соответственно при рефакторинге придется искать, в каких стримах используются специальные свойства ее прототипа, а не просто места вызова.


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


Ну и не могу не добавить, что в реальном проекте, судя по статье, это превратится в кромешный ад — combine, use, attach, restore, forward… Все эти стримы работают параллельно, и если страничка сложная и их сотня, даже самый искушенный разраб не сможет составить цельную картину… Зачем это все

А напишите как оно будет выглядеть, если в fetchData нужно передавать параметры? Какой-нить id из props'ов. А то там что-то странное с useState делается — похоже что его вместо useMemo пытаются использовать.

Если по каким-то очень странным причинам нужно к компоненту, использующему MobX observable нужно "подмешивать" пропсы, которые не observable — их можно записывать/обновлять через банальный useEffect(() => { setValue(value) }, [props.value]). Но это в принципе странный подход, который едва ли стоит использовать, за исключением особо извращенных ситуаций, которые я себе даже и представить-то не могу с ходу.


Традиционный простейший подход — если уж мы пишем с MobX, то в компоненты вместо любых пропсов мы передаём экземпляр стора/стейта, со всеми необходимыми observable и всем остальным, что там может понадобиться. И управляем данными полностью отдельно от вопросов рендера: модель построена отдельно (все сторы, по необходимости увязанные друг с другом в структуры), рендер — отдельно, в сложных случаях можно даже явно выделить структурный слой Presenter из MVP и связывать определенные сторы с определенными компонентами в нём. Подход с useState из кода выше — это создание локального для компонента observable state, и это приём из разряда "можно, но обычно не нужно". Документация MobX уже некоторое время в явном виде говорит нам "You might not need locally observable state".

А в каком смысле он "странный"?.. Это же вроде как раз стандартный общепринятый подход.


pages→user-details.tsx→<UserDetails userId={router.params.id}/>

Вообще, после небольшого знакомства с mobx у меня такое ощущение, что оно очень плохо идеологически сочетается с react'ом. Постоянные попытки натянуть сову на глобусизнасиловать react :)
Весь раздел доки "MobX and React" выглядит как набор хаков и список граблей, которые надо помнить и обходить.
В этом плане redux/effector, как мне кажется, лучше за счёт явной декларации зависимостей.

А в каком смысле он "странный"?.. Это же вроде как раз стандартный общепринятый подход.

Это стандартный общепринятый подход, если вы пишете на голом реакте. Самый что ни на есть react way, да. Реакт, как можно на самом деле догадаться из названия, тоже предоставляет абстракции реактивного программирования. Просто они прибиты гвоздями к компонентам, а потому убогонькие — не всё на фронте является компонентом. Но вы можете честно натянуть сову на глобус, и писать на голом реакте.
Но если вы берете другую либу для реактивности, то продолжать частично использовать реактивность реакта выглядит в высшей степени корявым решением. Вы же не берете редакс, чтоб в половине проекта использовать connect(), а в другой половине — нет? И не подключаете одновременно lodash и underscore, чтоб "по настроению" пользоваться функциями то одной либы, то другой?


Вообще, после небольшого знакомства с mobx у меня такое ощущение, что оно очень плохо идеологически сочетается с react'ом.

Почти всё плохо сочетается с реактом. И это не проблема "почти всего", кстати, а проблема другой стороны.


В этом плане redux/effector, как мне кажется, лучше за счёт явной декларации зависимостей.

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

Вы же не берете редакс, чтоб в половине проекта использовать connect(), а в другой половине — нет?

Крупные приложения, использующие redux, со временем настолько сильно минимизируют его использование, что возникает вопрос — чего больше useState или connect/useDispatch+useSelector.


Плохой пример в общем. Как раз redux ни разу не отменяет локальный react стейт.


Мы так вообще 9 к 1 уже пишем. 1 redux, 9 useState.

Это потому, что при повальном использовании редакса придётся писать гораздо больше кода, да и по производительности оно хуже, если все 100% компонент зацепить коннектом. Естественно, что взяв изначально убогое решение, потом начинаются попытки с него съехать по максимуму.


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

Мы так вообще 9 к 1 уже пишем. 1 redux, 9 useState.

Вопрос такой, зачем вы вообще используете redux и useState???
Вы же вроде как в курсе что такое MobX на практике.
Вопрос такой, зачем вы вообще используете redux

Нам нужно глобальное иммутабельное нормализованное хранилище. Redux это умеет, и умеет хорошо. Если его выкинуть и писать своё решение — то получится снова redux.


Или если выкинуть всё приложение и начать с нуля то получится Apollo. Я лично с ним не работал, но из того что я встречал взрослого про graphQL — мне кажется он идеально подходит к нашим задачам. Но увы, коней на переправе так просто не поменять.


и useState???

Мы в целом очень довольны хуками. Взяв псевдо-декларативный подход многие вещи стали решаться проще и очевиднее. Того ада, в который легко было попасть с классами — больше не наблюдаем. Всё прекрасно дробится, отлаживается. Но есть и свои минусы… Особенно высокий порог входа.


Вы же вроде как в курсе что такое MobX на практике.

Потому и не используем. Я на хабре писал про мой опыт с Knockout и с Vue. Возможно задействуем где-нибудь в мелком проекте Vue3 поиграться.

Нам нужно глобальное иммутабельное нормализованное хранилище

Зачем?
Того ада, в который легко было попасть с классами — больше не наблюдаем

Какой ещё ад в классах?)
Потому и не используем.

Почему «поэтому»?
Я на хабре писал про мой опыт с Knockout и с Vue. Возможно задействуем где-нибудь в мелком проекте Vue3 поиграться.

1) Причем тут Knockout? Это небо и земля.
2) При чем тут мелкий проект или крупный проект?
3) Что за опыт с Vue и Knockout?
Причем тут Knockout? Это небо и земля.

Я не вижу никакой принципиальной разницы между RxJS, KnockoutJS, Vue2, Vue3, MobX и прочими observable-ориентированными решениями. Суть одна. Отличаются обёртки.


По остальным вопросам не вижу смысла дискутировать.

Я не вижу никакой принципиальной разницы между RxJS, KnockoutJS, Vue2, Vue3, MobX и прочими observable-ориентированными решениями. Суть одна. Отличаются обёртки.

Учитывая, что разговор тут не в философско-академическом смысле, а в смысле "почему не используете" — уж простите, но такой ответ звучит как "Рабинович напел, не понравилось". Или "в теории, разницы между теорией и практикой нет".


Несмотря на то, что FRP в общем-то работает везде одинаково, разница в приёмах решения проблем и вообще написания кода между RxJS и MobX (за эти могу лично ручаться) — просто огромна.

разница в приёмах решения проблем и вообще написания кода между RxJS и MobX (за эти могу лично ручаться) — просто огромна.

Не спорю. Всё так. Не отрицаю. Конечно отдельно взятый framework может сильно отличаться от других. Но многие core вещи будут общими именно ввиду самого подхода. Потоки данных можно лихо закрутить даже во Vue2, несмотря на то что он не даёт прямого доступа к observable.


Мне лично пока куда больше заходит immutability и всякие игры с dependencies, с weakMaps. Нежели чистая реактивность на подписках. И чем дальше я занимаюсь фронтом, тем больше я тяготею именно к FP.

Мне лично пока куда больше заходит immutability и всякие игры с dependencies, с weakMaps. Нежели чистая реактивность на подписках. И чем дальше я занимаюсь фронтом, тем больше я тяготею именно к FP.

Ууууу понятно :D
И чем дальше я занимаюсь фронтом, тем больше я тяготею именно к FP.

Чем дальше, тем страшнее и страшнее будущим наследникам ваших проектов :D
С другой стороны, это отличный повод переписать всё с нуля для будущих наследников, а это лучшее что может быть в разработке, писать код с нуля.
А напишите как оно будет выглядеть, если в fetchData нужно передавать параметры? Какой-нить id из props'ов.

Вот так
codesandbox.io/s/modest-turing-ojpi9?file=/src/App.tsx

А то там что-то странное с useState делается — похоже что его вместо useMemo пытаются использовать.

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

useMemo не используется для этих целей потому-что:
You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render
const [state] = useState(() => new ItemsState(props));

вот это, кажется, работать не будет — при изменении props'ов новые не подтянутся. Я так понимаю, тут надо что-то типа


useEffect(() => {
  state.props = props
}, [props])

А, прошу прощения, не заметил ниже


state.props = props

:)

Вы говорите про реализацию, когда fetchData вызывается в конструкторе стора, но никто не мешает вызывать отдельно:


 const [state] = useState(() => new ItemsState());

useEffect(() => { state.fetchData(props.userId) }, [props.userId])

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


const userData = state.getUserById(props.userId)

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

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

Это скорее минус. Один дурак может сделать так, что 100 мудрецов потом не разберут.

Очень странно, что добавленные 2 строчки замыкания приблизили его к вариунту MobX :)

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

Я смотрю ты под всеми постами про стейт менеджеры пишешь про мобикс. Это хобби такое?

Вы рассматривали возможность вообще не использовать библиотеки? Например useState через useContrext не хватило для ваших задач?

Использование контекста очень сильно бьёт по производительности. Особенно в таких вещах как state manager. Т.к. любое изменение контекста вызывает re-render всего поддерева.

Спасибо за статью с примером, теперь не нужно объяснять другим, "почему effector хуже mobx", можно сразу ссылку на эту статью давать. Огромная куча лишнего кода, условностей, соглашений, а от этой композиции сторов проект скоро в лапшу превратится. И перфоманс там явно никакой. Сложность придуманных решений обеспечит высокий порог входа, и когда команда поменяется, все это будет считаться легаси, так как сильно зависит от специфики инструмента и подходы им ограничены, при этом миграция на другую схему работы с данными будет практически невозможной.


Недостатки mobx высосаны из пальца: первый (ручные @observable, @action) автоматизируется makeAutoObservable; второй ("нужно просто положить данные из ответа на запрос в хранилище, без взаимодействия с компонентом") — следствие отсутствия архитектуры, запросы нужно посылать из api-слоя, а результаты обрабатывать в экшенах (записывать данные в стор), реактовые компоненты здесь могут быть только триггером старта загрузки данных, обрабатывать в них ответ и не нужно; третий — что для отслеживания состояния запроса нужно вручную записывать в локальные стейты параметр isLoading, тоже следствие отсутствия архитектуры — экшены это обычные функции, и можно добавить в них как в подтип объекта observable-свойство, к примеру data или state, в которое записывать все, что необходимо — количество вызовов, статус обработки, стек ошибок, стратегию перезапросов и т.п., универсально для всех или уникально для каждого. И никакого оверхеда по TS, и работает отлично с SSR (в котором можно просто пробежаться по actions и отдать страницу, когда все экшены перейдут в состояние finished)


componentWillMount() { this.context.actions.fetchUsers() }

const { actions, state } = this.context;

if (actions.fetchUsers.state.isLoading) return <Loader />

return <>{state.users}</>

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


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

Двоякие впечатления от статьи, с одной стороны написано очень приятно, с другой — с посылом я полностью не согласен)


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


Производительность.


и плюс ко всему не «бил» по производительности нового проекта

"Производительность" библиотеки это по факту 2 вещи:


  1. Размер либы (влияние на скорость загрузки)
  2. Скорость работы функций либы и выстраиваемой архитектуры (рантайм-перформанс)

По первому пункту (размеру) эффектор выигрывает. Но только у mobx.
Приведенный размер правда неверный — https://bundlephobia.com/result?p=effector@21.7.5
Но и правда разница с mobx в 2 с лишним раза — https://bundlephobia.com/result?p=mobx@6.0.4


А вот redux меньше — https://bundlephobia.com/result?p=redux@4.0.5


Что касается mobx — тут есть одно большое "но". Примеры-то на react…
Что означает что вы грузите react. И хоть сам react может заманчиво прикидываться 7-килобайтной библиотекой, примерно как redux, но это не так :)
https://bundlephobia.com/result?p=react-dom@17.0.1


Т.е. выигрыш будет мизерным по сравнению с размером ui-фреймворка. Если проект реально маленький (и планируется его оставить таким) и время загрузки важно — можно взять vue/svelte.
Если же нет — качественный код лучше чем быстрый. Оптимизировать всегда успеете, обещаю :)


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


У redux тут есть известные боттлнеки и правда, когда в connect привязано слишком много компонентов.


Насчет effector — не возьмусь говорить, тк пока не понятно как выглядит "хорошая архитектура" на этом стейт-менеджере. Пока никто не возьмет его в продакшен-проект крупного масштаба, авторы библиотеки и дальше смогут говорить что перформанс хорош)


А у mobx тут всё хорошо. Всё в итоге сводится к тому что точечные обновления куда эффективнее чем обновлять целое поддерево компонентов.


"Плюсы" по сравнению с mobx


Нет надобности создавать отдельные классы-модели, прописывать им интерфейсы. Создали хранилище, создали событие, подписали событие на хранилище — готово!

Откровенно не понимаю что имеется в виду.
Да, пожалуй, если вы пишете на чистом js — можно ничего не писать и кидаться объектами без сигнатуры. Но интерфейс же например для юзера описан даже в статье. Просто в mobx он был бы например вероятно описан в классе. Не вижу тут разницы в boilerplatе.


А если речь о том что тут 2 стора вместо 2 свойств на классе — то это явно вкусовщина. Ну было бы не 2 переменные а 2 свойства, количество кода бы не изменилось, просто название писать в другом месте.


При грамотно созданных моделях в компоненте не нужно страдать и отслеживать все свои телодвижения по обновлению хранилища. Подключили его в компонент — он всегда актуален и перерисовывается при каждом обновлении хранилища. Никаких тебе Mobx-овых @action, @computed и прочей ручной настройки

Так в mobx тоже же нет ручной настройки в компоненте. @action, @computed — это про модель. И это не ручная настройка, это тот же самый бойлерплейт.
А в mobx@6 реально уже есть makeAutoObservable, т.е. декораторы можно и не писать.


Да и в целом в проекте я использовала версию Effector 21.5.0. То есть ребята мажорно обновляли свой проект 20 раз. Это очень существенно!

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


Итого
Если дать выжимку ощущения от статьи — кажется что все "недостатки" mobx (кроме может быть размера, но про то что это вряд ли важно в данном конкретном случае я написал выше) — очень субъективные негативные впечатления, вызванные совершенно другими вещами (aka непонимание какую логику в mobx надо уносить в модель и как), а не реальные проблемы фреймворка.
Вероятно то что mobx дает возможность писать "честные" классы, сбило автора с пути и заставило думать что в mobx принято делать действия в компонентах, но это не так. В целом правильный пример работы, например, со статусом запроса в mobx куда ближе к тому что есть в effector (записываем текущий статус в observable поле модели, в компоненте используем только его, не используем промис, не делаем запрос из модели).


Что касается того примеров effector в статье — он кажется слишком громоздким чтобы быть user-friendly. Уменьшение boilerplate-кода со стороны незаметно, а вот выросшая сложность чтения кода — да.


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

Sign up to leave a comment.