Vuex – решаем старый спор новыми методами

    Во Vuex есть одна популярная практика — не использовать mapState и mapMutations. Вообще. Вместо это мы сразу же при создании нового значения в store делаем для него геттер, а для каждой мутации – экшен.


    Что-то наподобие:


    export default new Vuex.Store({
      state: {
        // 1
        count: 0
      },
      getters: {
        // 1
        count: (state) => state.count
      },
      mutations: {
        // 2
        increment(state) {
          state.count++;
        }
      },
      actions: {
        // 2
        increment({ commit }) {
          commit('increment');
        }
      }
    });

    Холивара не избежать. Это уже доказала вот эта статья. Опять в интернете кто-то не прав, и я должен доказать почему. И я докажу.


    Почему нужно писать геттер на каждое значение в state, а на каждую мутацию – писать экшен?


    На это есть две причины. Первая: мы упрощаем компоненты и убираем неразбериху.


    import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
    
    export default {
      computed: {
        ...mapState(['count']),
        ...mapGetters(['text'])
      },
      methods: {
        ...mapMutations(['increment']),
        ...mapActions(['fetch'])
      }
    };

    Представьте, что этот компонент в разы больше. Как вы будете определять какие методы из store — это мутации, а какие экшены? Постоянно лазить в store.js, чтобы проверить? Может, на бумажке запишите? :)


    Можно ещё naming convention придумать, типа incrementMutation, decrementAction. Вроде проблему решили, но… как бы, вы сами понимаете. Мы и так два метода пишем, где по-хорошему должен быть один, так ещё и имена у нас супер длинные. Не говоря уже о том, что в <template> они выглядят отвратительно.


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


    Есть ещё и вторая причина, более концептуальная.


    Какой самый первый принцип написания кода изучает любой программист? DRY — Don't repeat yourself. То есть не повторяйся. Почему? Потому что, если придется потом что-то менять, менять придется в нескольких местах. И чем больше приложение, тем сложнее сделать какое-либо изменение.


    А теперь представим, что при добавлении новой функциональности нам пришлось поменять просто получение значения из state на геттер или поменять мутацию на экшен. Нам придется проходиться по всем компонентам, которые используют эти значения и менять mapState на mapGetters, mapMutations на mapActions и т.д. И боже упаси, если в каком-то компоненте до этого какой-то из методов не использовался. Придется его импортировать, добавлять в компонент и т.д. и т.п.


    Короче, причин для нашего решения достаточно.


    Однако, есть одна "небольшая" проблема — наш store теперь выглядит как свалка, где каждое новое значение в лучшем случае удваивает количество кода.


    С этим:


    1. можно смириться;
    2. можно распихать все по модулям, скрыв проблему (что равносильно тому, чтобы открытый канализационный люк прикрывать картонкой);
    3. можно поступить более радикально и отказаться от vuex в пользу другого способа хранения данных (у Composition API, например, уже есть интересное решение) и уже разбираться с проблемами других библиотек.

    У меня есть четвёртое решение. Если вы до сих пор не поняли, что эта статья — реклама, то готовьтесь: сейчас будет рекламная интеграция.


    Благо рекламирую я open source. А точнее аддон, который я недавно написал (аддоном я его называю, чтобы не его путали с плагинами).


    Называл я его vuex-map. Чтобы и понятно было, и писать import ... вам много не пришлось.


    Что он делает? Предоставляет вам два новых метода mapData и mapMethods (названия обсуждаются, вы можете на них повлиять тут).


    mapData заменяет функционал mapState и mapGetters. Вы пишете mapData(["count"]), если такой геттер есть, он запускает его, если нет, ищет такое значение в state.


    Аналогично, mapMethods возвращает экшен, а если такого экшена нет, комитит мутацию с таким названием.


    На самом деле, это очень простая идея. Удивительно, что разрабочики Vuex сразу не добавили её в ядро библиотеки. Благо занимает она всего строчек 100-150, не больше (это, если что, очень мало). А при этом решает все вышеперечисленные проблемы.


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


    При этом никаких лишних геттеров типа count: state => state.count и т.п. у вас нет.


    Что насчёт DRY? Тут тоже всё понятно. Если вам вдруг понадобился геттер или экшен, вы просто его добавляете, в компонентах ничего менять не надо. Новые геттеры и экшены просто "затемнят" значения и мутации с тем же названием.


    В каком состоянии находится аддон? Ну с момента, как я его дописал, прошло около суток, поэтому судите сами. :) Тесты на подходе, если вы читаете эту статью, значит где-то там я уже пишу тесты. Тесты написаны.



    Предлагаю всем попробовать и оценить. Буду благодарен за любые отзывы и любой вклад. Спасибо.


    Если вам так же, как и мне, нравится данная функциональность, и вы хотите видеть её в ядре библиотеки vuex, переходите по данной ссылке, там проходит обсуждение по этой теме.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 18

      0

      не хотел вас расстраивать, гляньте pathify
      Использую его, доволен.

        0

        Я посмотрел, и я не расстроен.
        vuex-pathify действительно очень крутая библиотека. Классный синтаксис а-ля Laravel и множество разных функций, уменьшающих бойлерплейт.


        Есть несколько особенностей.


        • Она весит столько же, сколько весит весь vuex. Неминифицированный, даже больше. Мне лично, наверное, было бы все равно, но кто-то будет недоволен;
        • Синтаксис классный, но он свой. Его надо учить, к нему надо привыкать, и человек со стороны, не знающий эту библиотеку его не поймет.

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

        +1
        На мой взгляд, использования экшенов вместо мутаций является небольшим антипатерном. Потому что экшн всегда возвращает промис. Даже если использовать их для синхронных изменений, получаем небольшой оверхед на ровном месте. Так же, есть потенциальная опасность при рефакторинге экшена сделать его асинхронным, забыв в месте вызова добавить ожидания выполнения и получить race condition в коде. А если ввести за правило, что экшены всегда для асинхронного кода, то dispatch без await будет не допустим.
        Да, я знаю, что эта давняя тема, такая же, как использование констант для имён мутаций. В целом, считают, принятие того или иного подхода должно отталкиваться от реалий проекта: размер команды, объём кода, требования к скорости разработки.
          0

          Если открыть документацию Vuex про действия (экшены), то там написано вот что:


          Действия — похожи на мутации с несколькими отличиями:
          • Вместо того, чтобы напрямую менять состояние, действия инициируют мутации;
          • Действия могут использоваться для асинхронных операций.

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


          Действия – это просто высокоуровневые обертки над мутациями. Если мутация делает что-то конкретное со state, притом обязательно синхронно, то действия придуманы как раз для того, чтобы быть "контроллерами" над мутациями.


          Действия должны выглядеть как API для взаимодействия со store, а не просто как аналог мутаций, только с асинхронными возможностями.

            0
            dispatch всегда вернёт промис, хотя, конечно, никакого серьёзной нагрузки это не несёт.
            Я просто сталкивался с кодом, в котором много вызовов dispatch сделаны в синхронном стиле. И проблема в том, что были места, где такое использование по недосмотру приводило к ошибкам.
            Просто часто для программиста вся разница между вызовом действия и мутации — просто вместо mapActions написать mapMutations. Собственно, ваша библиотека и убирает эту грань, но мне обычно хочется чётко понимать, что тут dispath делает асинхронное обращение к API, а тут commit сразу меняет state. Хочу контролировать всю «асинхронщину» в коде, т.к. ошибки такого рода сходу не видны.
              0
              Проверил что возвращает простой action в котором явно ничего не возвращается, а только вызывается простой mutation. И действительно он возвращает какой-то промис. Так что evgeniyPP вы ошибались.

              Но я тоже придерживаюсь правила:
              Действия – это просто высокоуровневые обертки над мутациями. Если мутация делает что-то конкретное со state, притом обязательно синхронно, то действия придуманы как раз для того, чтобы быть «контроллерами» над мутациями.
              так удобней, знаешь, что если надо править работу модуля, меняешь мутации и уверен, что остальное не сломается.
              Когда же нужна асинхронность, то в теле action явно возвращаю promise.

              Что же касается сведений:
              Я просто сталкивался с кодом, в котором много вызовов dispatch сделаны в синхронном стиле. И проблема в том, что были места, где такое использование по недосмотру приводило к ошибкам.
              то могу сообщить, что кучу раз вызываю dispatch в «синхронном стиле». Ни каких особых сложностей, и поводов к ошибкам. Он либо ничего не возвращает (кроме промиcа разработчиков) — либо промис, который мы хотим. При реактивном хранилище нам по большей части и даже не надо возвращать ни чего. Даже если action асинхронный, то все изменения произойдут не зависимо от вызывающего кода. А реактивность нам обновит данные компонентов.

              Хочу контролировать всю «асинхронщину» в коде, т.к. ошибки такого рода сходу не видны.
              Понимаю вашу позицию, тут как вкус или привычка. Мне удобней рассматривать actions как API. И, кстати, и вся асинхронщина как раз оказывается в actions.

              Кстати интересно в чем смысл возврата непрошеного промиса в action?
                0

                Да, я был не прав.
                Я не знал, что там в ядре прописано return new Promise. Я имел в виду, что не все экшены обязательно должны быть асинхронными.

                  0
                  В целом, я хотел пояснить, почему в самом Vuex есть такое разделение, и вам пришлось написать библиотеку. Вы молодец, что выработали для себя подход и поделились своим решением. Мнение о том, что из компонентов надо вызывать только действия, популярная (вот, например, обсуждение в github vuex). Кстати, вроде порой всплывают разговоры, чтобы объединить мутации и действия, но мне кажется не так просто подружить синхронный и асинхронный подход.

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

                  Например, у меня была задача сохранять параметры приложения в url. Классически, это решается через vue-router, проброс url-параметров в props-компонента, смена параметров через $router.push, watch-ере на $route и так по кругу.

                  В моём случае мне не хотелось переносить хранение части данных вне стора, хотелось сохранить state как единственный источник правды. И не хотелось менять существующий код работы с хранилищем. Поэтому я сделал зеркалирование некоторых параметров state в url при их изменении. Самое интересное, во vuex даже был подходящий метод (watch), позволяющий такое сделать. И это действительно классно, что vue позволяет гибко расширять функционал.
          –1
          Фразами вроде «А теперь представим, что» и «А вдруг» вымощена дорога в overengeneering.

          Конкретно во vuex и правда неплохо работают упомянутые naming conventions, например mutations капсом, actions кэмелкейзом — то, что чаще всего встречал.
            0
            > Фразами вроде «А теперь представим, что» и «А вдруг» вымощена дорога в overengeneering.

            +1
            А давайте напишем фреймворк для фреймворка? )

            Конвенции и настроенный линтинг/анализатор решит ненужные проблемы.
              0

              А стандартные мапперы (mapState и др.) – это тоже отдельный фреймворк фреймворка? Только уже встроенный внутрь заранее.
              Вот vuex-pathify – это действительно фреймворк фреймворка. Без вопросов (хотя я не имею ничего против фреймворков фреймворков и т.д.)
              У меня лишь пару дополнительных методов, которые ничего принципиально не меняют.

                0

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


                В итоге какое то не понятное правило для бабел и куча ошибок в существуюших проектах

                  0

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

                    0
                    Я не считаю, что это ошибка.

                    А давайте призывать писать новый класс на каждый геттер? Не ну а ч0, там и валидация потом (через класс валидации конеш). Мало ли что нерадивые девелоперы в параметры гетера передадут.


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

            0
            У нас по конвенции все actions возвращают промисы, и только они могут вызывать мутации.
            А mapData, да, наверное можно использовать что-то подобное.
              0
              ЗЫ. Трейты здло. Каждый хочет показать как он вёртко обращается с новыми функциями языка, но не каждый потом поддерживает свой (или чужой) код
                0
                Странно, я думал старый спор ведется вокруг темы «НЕ использвоать Vuex вообще»
                  0

                  Не, vuex норм. Просто не надо переусердствовать. Можно и абстракцию на абстркцию которая на абстракцию вывести.


                  НО ЗАЧЕМ ?

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

                Самое читаемое