Эксперимент: Redux от мира ООП +8


В интернетах давно ведётся священная война между адептами Функционального Программирования и ООП, Redux и MobX, React и Angular. Многие годы я обходил её стороной, но теперь эта тема коснулась и меня.



Сейчас Redux используется в сотнях тысяч проектах, сообщество постепенно становится менее гибким. Если ты приходишь в компанию, где, к примеру, активно используется React, то скорее всего, к нему в комплекте как правило идёт и Redux. К этой библиотеке накопилось достаточно много хейта, но использовать что-то другое уже сложнее в силу многих причин. Чем так плох Redux и почему без него ещё хуже, хорошо выражено во многих статьях, но как правило, авторы эмоционально навязывают использовать что-то, что им самим нравится больше. Пожалуй, наиболее объективно написано в переводе "Совершенствуем Redux". Но особенно хочется выделить одну цитату:


Redux, в принципе, простая и небольшая библиотека с крутой кривой обучения. На каждого разработчика, который овладел и извлек выгоду из Redux, погрузившись в функциональное программирование, найдется другой потенциальный разработчик, запутавшийся и думающий «это все не для меня, я возвращаюсь к jQuery».

И я один из последних, к слову.


Цель любой библиотеки: сделать сложное простым при помощи абстракции.

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


Зачем


… придумывать новый велосипед?


На самом деле, я недавно проникся самой важной фишкой ФП (и Redux) — иммутабельностью состояния со всеми вытекающими из неё плюсами. Но моё ООП головного мозга не позволяет мне писать столько лишнего и запутанного кода, который заставляет писать Redux.


Кстати, в этом плане мне гораздо больше нравится Vuex, код получается приближенным к тому, что описано в статье выше, но даже он не идеален. Для vuex существует несколько @декораторов, которые представляют модули в виде класса, но ни один из них не разрешает наследование, поэтому я написал свой собственный декоратор с поддержкой такой возможности. Как видите, моя погоня за совершенным кодом началась уже тогда. Наследование — такой же важный аспект ООП, как и полиморфизм, и я постараюсь показать в этой статье пример из реальной жизни, почему стейты должны его поддерживать.


Store


Итак, идеальный код, по моему мнению, выглядит примерно так:


import {Store, state, Mutation, Action} from "@disorrder/storm";

export default class Account extends Store {
    @state amount = 0

    @Mutation setAmount(val = 0) {
        return {amount: val};
    }

    @Action async updateAmount() {
        let {data} = await api.get("/user/account");
        // Вариант 1
        this.setAmount(data);
        // Вариант 2
        this.amount = data;
        // Вариант 3
        this.mutate({amount: data}); // Вообще, этот метод использовать не придётся.
        // Все 3 варианта делают одно и то же, но лучше использовать первые два.
    }
}

Это библиотека, которую я написал, вдохновляясь всем этим зоопарком технологий и (теперь) ненавистью к Redux. Терминология vuex кажется мне наиболее понятной и подходящей, поэтому я выбрал её.


Класс Store создаёт внутри себя иммутабельный state, к которому нет доступа напрямую. Изменить состояние можно с помощью внутреннего метода mutate(), который создаёт новую изменённую копию state, но в большинстве случаев пользоваться им не придется.


Декоратор @state создаёт интерфейс (геттер и сеттер) для управления значениями в state.


Mutation — примерно то же самое, что и reducer в терминологии Redux, отвечает за изменение информации в state. В идеале, никогда не должно быть "пустых" изменений. В отличие от Redux, вызов какого-либо action влияет только на тот класс, в котором он был вызван. В том же Redux мне никогда не доводилось видеть, чтобы один action.type использовался в нескольких редьюсерах. Mutation стоит воспринимать исключительно как минорное изменение инкапсулированного состояния.


Action же — почти всегда асинхронное действие. Вызов метода из API, например, или даже другого action. Конечно, он может быть и синхронным, никаких особых ограничений нет, но тогда стоит задать себе вопрос, не должна ли это быть мутация.


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


Контекст приложения


Библиотека разработана так, чтобы не зависеть от других библиотек и фреймворков вроде React или Vue. Не нужно вызвать функцию connect (как в Redux). Достаточно импортировать модуль в нужное место и просто работать с ним!


// src/store/index.js
import User from "./User"
export const user = new User()
  . . .

// src/store/User.js
export default class User extends Store {
  // ...
}

// src/App.js
import user from "./store";

class App extends Component {
    componentDidMount() {
        user.subscribe((mutation, oldState) => {
            this.setState({});
        });
    }
}

Вот теперь всё заработает. Метод subscribeподписывает на любые мутации именно модуля user, но в первом аргументе mutation можно проверить, какие именно объекты были изменены.


Collection pattern


Наконец, добрались до примера, где у меня появилась острая необходимость в наследовании. Пока что это единственный пример из моего опыта, но даже его уже хватает. Уж слишком часто этот код приходится копипастить из проекта в проект, поэтому ему уделяется подробное внимание. А именно, хранение многочисленных коллекций, с которыми можно производить CRUD. Полный код примера можно посмотреть здесь. (Возможно, в первые дни после публикации полного примера нет, но он обязательно появится)


export default class Collection extends Store {
    url = "/"
    pk = "id" // Primary key

    @state items = {}
    @state indices = [] // itemIds

    // Из-за обращения к объектам в стейте напрямую получилось две мутации. Это может привести к лишнему срабатыванию рендера.
    @Mutation __add(item) {
        const id = item[this.pk];
        this.items = {...this.items, [id]: item};
        this.indices = [...this.indices, id];
    }

    // Так-то лучше
    @Mutation add(item) {
        const id = item[this.pk];
        const items = {...this.items, [id]: item};
        let mutation = {items};

        const rewrite = id in this.items;
        if (!rewrite) {
            mutation.indices = [...this.indices, id];
        }

        return mutation;
    }

    @Action async create(data) {
        let item = await api.post(this.url, data);
        this.add(item);
    }

    @Action async getById(id) {
        let item = await api.get(`${this.url}/${id}`);
        this.add(item);
    }

    @Action async getList(id, params) {
        let items = await api.get(this.url, {params});
        items.forEach(this.add.bind(this));
    }
}

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


export default class Users extends Collection {
    url = "/users"
}

Всё. В этом прелесть и сила наследования. Такой подход оставляет и пространство для манёвра: можно переопределять отдельные методы, сохраняются парадигмы SOLID, DRY, KISS — всё, как мы любим.


Преимущества


  • Исходный код укладывается всего в 150 строк. Возможно, ещё столько же добавится в будущем, но не сильно больше. Однажды мой знакомый сказал, что "Redux прост, как палка". Однажды я попрошу метафору для своего велосипеда!
  • Стиль кода навязывает модульную систему. В проектах с Redux часто reducers и actions лежат в разных папках.
  • Никаких switch-case
  • Никаких проблем с асинхронными функциями

Недостатки


  • К сожалению, декораторы ещё не работают из коробки. Придётся настроить Babel. Но есть функция createStore, которая позволяет обойтись без них.
  • Слабая функция subscribe, возможно стоит добавить watcher на отдельный объект стейта
  • был ещё один, но пока писал, успел забыть

P.S.: Не претендую на звание прорывной библиотеки, отталкивался от самого кода, который мне кажется наиболее удобным. Это мой личный эксперимент и мысли, которыми я хочу поделиться. Любая критика принимается, просто держу в курсе.


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

Какую библиотеку выберешь в новом пет-проекте?

  • 35,2%Redux (saga, thunk...)32
  • 33,0%MobX30
  • 3,3%Flux3
  • 12,1%Plain js object11
  • 1,1%Storm1
  • 5,5%Напишу в комментах5
  • 9,9%Не использую глобальные состояния9




Комментарии (43):

  1. VolCh
    /#21391062

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


    1. Сложнее будет обеспечить консистентность. По сути тогда внешний action не должен изменять состояние напрямую, только дёргать внутренние.
    2. Непонятно когда уведомлять подписчиков после каждого внутреннего или после последнего.
      Скорее для редких случаев нужен какой-то отдельный batchAction, который доступа к изменению стейта не имеет, но агрегирует сообщения об изменении стейта в одно.

    • Disorrder
      /#21391148

      Подписчики уведомляются после любой мутации, не на экшн.
      Согласен, что вложенные action — не православно, но у меня был момент в vuex, когда было 2 похожих экшена, но с разными аргументами. Не хотелось дублировать код, но пришлось, т.к. не было возможности выносить общую логику.
      Если экшн не вызывает мутацию, а вместо этого вызывает другой экшн, является ли он экшеном по семантике?

  2. Azirel
    /#21391106

    У меня есть собственноручно свелосипеденный dataStore, который отлично поддерживает SSR, асинхронность, всё такое — но при этом мутабельный на всю голову. Из boilerplate — необходимость в компоненте вызвать initStateFromStore в конструкторе и обязалельно наследоваться от DataComponent. А, ну и в SSR-модуле пробрасывать состояние на клиент, но там одна строчка на один вид хранилища.

    Им пока на петах и намерен пользоваться.

    «Глобального» состояния у меня вообще нет, впрочем, т.к. я считаю, что глобальное состояние — это как глобальные переменные. Всё, необходимое для более чем одного компонента пробрасывается через React.Context.Producer

  3. megahertz
    /#21391146

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

    • Disorrder
      /#21391162

      На самом деле, всё просто. На это есть как минимум, 2 причины:


      1. MobX сильно завязан на React, а я (теперь) пишу код не только на нём. Этого для меня уже достаточно.
      2. Вроде бы MobX не делает свои состояния иммутабельными.
        Не имел дел ни с MobX, ни с SSR, поэтому не уверен, совместимы две эти вещи или нет. Если нет, то это могло бы стать третьей причиной.

      • MaZaAa
        /#21391244

        Вы похоже вообще не знакомы с mobx и даже не пытались. MobX сам по себе вообще не привязан ни к реакту, ни к чему либо ещё.
        Связывает mobx с реактом уже отдельные пакеты mobx-react или mobx-react-lite.
        Связать реакт компоненты с mobx можно и без них, но вопрос нафига?)
        MobX именно мутабильный и реактивный, он не про иммутабильность. В этом его сильная сторона которая позволять писать минимальное количество кода, легко воспринимаемого и поддерживаемого.
        Вы можете очень легко и быстро с ним познакомится в действии, пожалуйста:
        codesandbox.io/s/ecstatic-cloud-l956n

        По поводу SSR, сам по себе SSR у реакта нереально убогий, медленный и очень CPU затратный, гораздо логичнее там где он реально нужен использовать puppeter (который будет рендерить страницу и ему по барабану реакт или не реакт) и складывать результат HTML в кэш и потом при запросе по урлу выплевывать из кэша уже готовый HTML.
        Вы получаете:
        1) Независимый (от фреймворков, либ и т.д. и т.п.) SSR который можно использовать от проекта к проекту.
        2) Максимально возможный быстрый ответ от сервера, потому что нужно только достать готовый HTML из кэша, что крайне важно для поисковых роботов и для людей которые хотят быстро увидеть контент.
        3) Вам не нужны для этого большие мощности и кластеры серверов чтобы обслуживать поедание CPU реактом на стороне сервера на каждый запрос. Отсюда экономия денег на инфраструктуре и мощностях.
        4) В разработке вам вообще ни каким боком не нужно думать о том, что у вас есть SSR и каким либо образом под него подстраиваться.

        • Disorrder
          /#21391556

          Ок, спасибо. Я не скрывал того, что не знаком с MobX. Мне хватило первого впечатления от официальной документации. Думаю, если бы всё было так очевидно, не было бы вопросов, подобных этому.

          • MaZaAa
            /#21391562

            Мне хватило первого впечатления

            Один из моих любимых «железобетонных» аргументов)))

      • ilyapirogov
        /#21391976

        А зачем вам вообще в SSR глобал стейт?

        • justboris
          /#21392530

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

  4. JustDont
    /#21391446

    Никогда не понимал, зачем люди намеренно гоняются за иммутабельностью везде. Она не бесплатна, наивная иммутабельность будет расходовать память (да чё, пусть все еще 4Гб прикупят, подумаешь), а оптимизированная, как тот же Immutable.js — тянет за собой ОЧЕНЬ немалое количество кода, выполняющего все эти оптимизации.

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

    ЗЫ: На практике я один раз сталкивался с тем, что страничка, показывающая сложную таблицу с инфографикой (где-то 2Мб исходного JSON) — сжирала добрую сотню мегабайт памяти на многократную копи-пасту исходных данных и наивно закешированные части DOM для переиспользования. На фоне того, что браузер на рендер и прочую песочницу сам пару сотен мегабайт отхватит — это конечно не было сильно заметно, но сам факт наличествовал. Если б у нас входные данные были бы не по паре мегабайт в худшем случае, а по паре десятков — наступил бы «ой». Просто потому, что кому-то очень сильно нравится иммутабельность.

    • Disorrder
      /#21391548

      Если требуется поместить в стейт большой тяжёлый объект и исходя из парадигм ФП использовать его как иммутабельный, скорее всего, что-то плохо спроектировано. В глобальных состояниях не должно быть много информации. Наверняка эта сложная таблица из Вашего примера нужна была только в одном месте, а значит, можно было обойтись локальным стейтом и не делать иммутабельной. Вообще, это 2 разных инструмента со своими характеристиками. Как сварка и соединение на болтах.

      • MaZaAa
        /#21391558

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

  5. IvanGanev
    /#21391478

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

    • MaZaAa
      /#21391552

      Потому что Реакт в качестве управления состоянием — ущербный. Зачем преднамеренно себе портить жизнь? Если хотите огребать все проблемы которые вытекают из иммутабильности, плюс в довесок проблемы с лишними перерендерами из-за этого, то вперед и с песней конечно, те кто придет к вам на проект или после вас, скажет вам больше спасибо)

      • Disorrder
        /#21391610

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

        • MaZaAa
          /#21391914

          Вы не понимате, у вас есть объект скажем из 500 свойств, они имеет в себе вложенные объекты тоже и есть 3 отдельных компонента, каждый из которых берет из этого объекта разные свойства для рендера, в том числе и те в которых вложены ещё объекты.
          Теперь вам нужно изменить свойство lastName у этого объекта и только 1 компонент из 3х работает с этим свойством. И логично, что только он должен перерендерится.
          Дальше думаю не нужно объяснять как это свойство нужно менять при иммутабельном подходе и при реактивном мутабельном, а так же как на это отреагирует реакт и что перерендерит. Что при этом будет с расходом памяти, с загрузкой процессора при клонировании объекта чтобы иммутабильно изменить у него одно свойство.

      • IvanGanev
        /#21391720

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

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

        • MaZaAa
          /#21391870

          Эта библиотека:
          — Не добавляет сложности к коду.
          — Не добавляет того, что приходится писать больше кода. Напротив благодаря ей вы пишете минимально возможное кол-во кода.
          — Не замедляет производительность. Напротив она оптимизирована из коробки и всё делает за вас.
          — Делает реакт реактивным.
          — Раскрывает на все 100% потенциал реакта.
          — Не имеет ограничений.
          — Не загоняет тебя в рамки.
          — Не нужно давать понять реакту где были изменения, а где нет, библиотека сама это знает и делает это за вас.
          — Она добавляет всего лишь 15kb gziped к размеру бандла.
          — и т.д. и т.п.

          Вопрос:
          Зачем преднамеренно отказываться от этого и ущемлять себя?? Это такой вид мазохизма или что?

          • IvanGanev
            /#21391944

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


            Для меня это одна из проблем mobx, однонаправленный поток данных Реакта это, в том числе, способ упростить понимание того что вообще происходит в приложении. Архитектура приложения соответствует тому как данные по нему текут, таким образом я всегда могу быть уверен в том какой компонент чем управляет. При применении mobx архитектура приложения НЕ отображает этого (она может это отображать, а может и нет).

            • MaZaAa
              /#21392044

              Раз вы так говорите, то значит вы одновременно намекаете, что и опыт у вас есть использования MobX в реальной жизни, и он доставил вам какие-то проблемы или неудобства.
              Конкретный пример пожалуйста приведите, в которой отражает то, что вы говорите.

              • IvanGanev
                /#21392116

                Пример чего? Я уже описал выше что мне не нравиться в mobx. Мне что, код коммерческого проекта показывать?

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

                Когда кто-то полагается на какой-то базовый инструмент, к примеру, на Реакт, нужно, для начала, разобраться в том как он работает сам по себе, как он управляет стейтами, и уже потом смотреть на сторонние библиотеки. Если сразу же начать юзать MobX то можно попасть в ловушку. Это как начать изучать Реакт до изучения JavaScript, это просто опасно.

                Когда новички читают категоричные фразы вроде:

                Потому что Реакт в качестве управления состоянием — ущербный. Зачем преднамеренно себе портить жизнь?


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

                • MaZaAa
                  /#21392152

                  При таком подходе можно даже не понять какую проблему mobx вообще решает.

                  MobX решает проблему управления состояния приложения. Точно так же как и Redux, React со своими встроенными средствами и т.д. и т.п. Я даже не знаю при каком кейсе можно подумать иначе…
                  Пример чего? Я уже описал выше что мне не нравиться в mobx. Мне что, код коммерческого проекта показывать?

                  Вы не описали ничего. Иначе я бы не задал уточняющий вопрос. Пример можно привести как на любом псевдо коде, так и словами. А если примеров нет, то значит вся проблема кроется в том, кто этот код пишет, а не в инструменте.
                  А вот это:
                  При применении mobx архитектура приложения НЕ отображает этого (она может это отображать, а может и нет).

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

  6. Fen1kz
    /#21392542

    Почему мне нравится redux и не нравится библиотека автора (на первый взгляд).


    1. Прозрачность дебага и экшенов.


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


    2. Синглтон. Тут просто до сразу свиданья. Особенно порадовал файл с тестами: типа берем counter из прошлого теста, засовываем туда count. Чтооо, кто так вообще делает?


    3. В отличие от Redux, вызов какого-либо action влияет только на тот класс, в котором он был вызван

      Слово Дэну — https://github.com/pitzcarraldo/reduxible/issues/8#issue-124545582


    4. В Redux стор синхронен. И это гигантский плюс — я могу гарантировать что после определенного набора экшенов, мой стор будет определенного вида. Нигде не повисло await api.add()



    Как-то так. Судя по тексту вижу непонимание ни реакта, ни редакса. Особенно по цитате "Цель любой библиотеки: сделать сложное простым при помощи абстракции.". И дальше выкидываем редакс потому что в нем сложно сделать что? Получать данные из апи? Оу, так жаль. Но ведь получать данные из апи и так просто.


    А вот, например, контролировать стейт просматривалки ПДФок — нет.


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


    PS: Написал комментарий, решил все таки поискать подтверждение своей правоты, чтобы не голословно рассуждать о том "для чего редакс". Вдруг авторы сами говорят типа "редакс он для уборки помещений и распития смузятины", а я тут зря выдумываю. Но, на второй странице: https://redux.js.org/introduction/motivation


    At some point, you no longer understand what happens in your app as you have lost control over the when, why, and how of its state.

    и


    This complexity is difficult to handle as we're mixing two concepts that are very hard for the human mind to reason about: mutation and asynchronicity. I call them Mentos and Coke.

    см. пункт 4

    • MaZaAa
      /#21393546

      В Redux стор синхронен. И это гигантский плюс — я могу гарантировать что после определенного набора экшенов, мой стор будет определенного вида. Нигде не повисло await api.add()

      Ага, обязательно, когда весь фронтенд асинхронный и все запросы к АПИ асинхронны. Ничего вы не гарантируете.
      Тем более, даже если где-то и повисло ваше await api.add() и что с того? Когда оно отработает, то либо вы уведите результат, либо нет, потому что например вы уже на другой странице, либо же было race codition и этот результат уже не актуален.
      В чем проблема то?

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

      Если у вас возникают такие потребности и вам по коду не понятно, что вызывается после тех или иных действий, для того чтобы поставить console.log'и где нужно, то это проблема вашей архитектуры и того, как вы пишете код. В MobX'e тоже есть MobX Dev Tools где можно смотреть что и как менялось, но я этим не пользуюсь от слова совсем, потому что код который можно писать благодаря нему в разы компактнее и понятнее, не надо распутывать головоломки и гадать на кофейной гуще. Он асинхронный и реактивный, если понимать что означают два этих термина, а в мире JS без этого никак, то ты на полную катушку используешь язык и то, как можно писать код.

      This complexity is difficult to handle as we're mixing two concepts that are very hard for the human mind to reason about: mutation and asynchronicity. I call them Mentos and Coke.

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

      • Fen1kz
        /#21393930

        Ага, обязательно, когда весь фронтенд асинхронный и все запросы к АПИ асинхронны. Ничего вы не гарантируете.

        Гарантирую. Редьюсеры синхронны — если экшен прошел через стор, значит он однозначно в состоянии Х. Даже если у меня где-то висит await api.add() — выполнение его ничего мне не поменяет без экшена.


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

        Ага, щас вот обкладывающие код console logами обезьяны мне будут про мою архитектуру рассказывать.


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

        • MaZaAa
          /#21394118

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

          Ну вообще то да, это основополагающая любого вэб и мобильного приложения. И от этого никуда не дется. Если вам это не по зубам на данный момент времени, то почему должны страдать остальные из-за ваших костылей и тонн строк единообразного кода?
          Гарантирую. Редьюсеры синхронны — если экшен прошел через стор, значит он однозначно в состоянии Х. Даже если у меня где-то висит await api.add() — выполнение его ничего мне не поменяет без экшена.

          Так этот висящий запрос выполнится и экшен вызовется и оно вам всё поменяет. Или вы делаете запросы к АПИ, потом получаете данные, но ничего с ними не делаете и не вызываете экшен чтобы их сохранить в хранилище и не вызываете экшен который скажет что isFetching теперь false и пользователь должен перестать видеть loader и должен увидеть данные?)

          • Fen1kz
            /#21394550

            Так этот висящий запрос выполнится и экшен вызовется задиспатчится и оно вам всё поменяет.

            Вот мы и разделили асинхронность и обновление состояния.

            • MaZaAa
              /#21394674

              Вот мы и разделили асинхронность и обновление состояния.

              Неужели? Вы делаете асинхронные вызовы результаты которых не обновляют состояние? И чем же это отличается этого?
              class ItemsState {
                  @observable isFetching = false;
                  @observable items = [];
                  @observable error = null;
                  
                  fetchItems = async () => {
                      this.isFetching = true;
                      
                      try {
                          const response = await api.get('/items');
                          this.items = response.items;
                          this.error = null;
                      } catch (e) {
                          this.error = e.message;
                      } finally {
                          this.isFetching = false;
                      }
                  }
              }
              

              или от этого в стиле того, что автор сделал
              export default class Collection extends Store {
                  @state isFetching = false;
                  @state items = {};
                  @state error = null;
              
                  @Mutation setItems = (items) => {
                      return { items };
                  };
              
                  @Mutation setError = (error) => {
                      return { error };
                  };
              
                  @Mutation setIsFetching = (isFetching) => {
                      return {
                          isFetching: isFetching;
                      }
                  };
              
                  @Action getList = async (id, params) => {
                      this.setIsFetching(true);
              
                      try {
                          let items = await api.get(this.url, { params });
                          this.setItems(items);
                          this.setError(null);
                      } catch (e) {
                          this.setError(e.message);
                      } finally {
                          this.setIsFetching(false);
                      }
                  }
              }
              


              Считайте что fetchItems и getList это ваш экшен который вы задиспатчили, чтобы получить данные с сервера.

              • Disorrder
                /#21394724

                мне нравится стиль кода MobX, можете считать, что я его слизал. Хотя на самом деле, я опирался на декораторы для Vuex.


                Различия не снаружи, а в реализации. В моём подходе общего больше именно с Redux, иммутабельность и всё, что из неё проистекает.

                • MaZaAa
                  /#21394750

                  Я привел этот код не в упреку вам, а я его привел в плане того, что разницы нет, тоже самое происходит что в этих 2х вариантов, что в варианте с Redux. Просто в Redux это скрыто за тысячей файлов и функций, а тут все наглядно и просто.

              • Disorrder
                /#21394770

                Кстати, вот этот код здесь лишний:


                    @Mutation setItems = (items) => {
                        return { items };
                    };
                
                    @Mutation setError = (error) => {
                        return { error };
                    };
                
                    @Mutation setIsFetching = (isFetching) => {
                        return {
                            isFetching: isFetching;
                        }
                    };

                Эти сеттеры устанавливает декоратор @state, не нужно их писать. Фактически, при вызове collection.error = "Shit happened!" автоматически сработает мутация, которая потянет за собой оповещение подписчиков.
                Так что по объёму кода получается 1-в-1

                • MaZaAa
                  /#21394796

                  Да я не сравнивал ваш код с MobX'ом =) Я просто привел стандартный пример асинхронного получения данных как примеры вариант MobX'a и ваш вариант. Fen1kz утверждает что Redux вообще не зависит от асинхронных вызовов и изолирован полностью. Там вообщем все написано в его сообщении и в ответе на его сообщение)

                  • Disorrder
                    /#21394816

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

              • Fen1kz
                /#21395904 / +1

                Мне реально надо объяснять чем


                const response = await api.get('/items');
                this.setItems(response.items);

                отличается от


                const response = await api.get('/items');
                dispatch(itemsLoaded(response.items))
                
                ...
                (response, items) => state.set('items', items)

                ?


                И что в одном из этих случаев разделения нет, а в другом — есть?

                • MaZaAa
                  /#21396060

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

    • Disorrder
      /#21394678

      1. Действительно у меня нет настройки, которая автоматически пишет в консоль вызов каждой мутации. Рано или поздно я её добавлю и это никак не отразится на текущем апи. Но что мешает подписаться на изменения и логировать то, что нужно (ну, или всё подряд)? В коллбеке приходит как изменение, так и старое состояние. Получить текущее состояние можно методом store.DEBUG_getState(). Как и в редакс, я предлагаю сохранять стейт иммутабельным, как раз исходя из плюсов этого подхода вроде логирования, чистых методов и т.д.


      2. Чем плох синглтон? Вообще, стор спроектирован так, что ты можешь создавать столько его экземпляров, сколько нужно. Прямо сейчас у меня нет времени, чтобы найти на гитхабе issue, где разработчики говорят о сложностях создания в редаксе нескольких одинаковых стейтов и методов для них. Вроде, там было что-то про counter_1, counter_2, counter_3…
        Тесты — ну, извините, я последний раз писал тесты лет 6 назад, не очень в этом хорош.


      3. Ужас какой!


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



      По поводу простоты. Редакс генерит много тупого кода из ничего. Action и Reducer это по сути то же самое. Чтобы вызвать dispatch, мне нужно определить вызов экшена в 4 местах. Паттерн использования switch-case внутри одной функции пришёл как полиморфизм из мира ФП. Обо всём этом написано в статье "совершенствуем Redux", ссылка на которую приведена в моём тексте. Похоже, вы с ней не ознакомились, прежде чем писать комментарий.


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

      • Fen1kz
        /#21395938 / +1

        Action и Reducer это по сути то же самое.

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

        • MaZaAa
          /#21396074

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

        • Disorrder
          /#21401210

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

          Вместо того, чтобы вайнить и минусить, лучше бы написали, в чём именно я не прав.

  7. Cobalt
    /#21393112

    А как же storeon от Ситника?

    • Disorrder
      /#21394706

      Не то пальто. У меня ООП, классы, наследование… А там?