image


Я люблю Redux


Именно благодаря Redux для меня началось путешествие в мир удивительного функционального программирования. И это первое из функциональщины, что я попробовал в production. Прошли те времена, когда я использовал DOM для хранения состояния и неуверенно манипулировал им с помощью jQuery.


Redux — это инструмент для управления состоянием приложения (state), который позволяет полностью отделить его от представления (view). Представление (view) становится производным состояния (state), которое предоставляет пользователю интерфейс для его изменения. Действия пользователя (actions) не изменяют состояние (state) напрямую. Вместо этого они попадают в редюсер (reducer). Это такая чистая функция, которая на основе предыдущего состояния (state) и действия (action) генерирует следующее состояние (state). Такой подход к обновлению данных во многом был вдохновлен архитектурой языка программирования Elm и концепцией однонаправленного потока данных Flux. Это, возможно, самая популярная JavaScript-библиотека для иммутабельного изменения состояния из тех, что существуют сегодня. Авторы Redux сфокусировались на решении одной единственной проблемы — управление состоянием приложения (state), и сделали это хорошо. Redux получился достаточно модульным, чтобы работать с различными библиотеками для отображения представления (view).


React использует аналогичный сфокусированный подход для представления (view), имеет эффективный виртуальный DOM, который можно подключить к DOM браузера, нативным мобильным приложениям, VR и прочим платформам.


Что бы создавать надежные, функциональные и легко отлаживаемые web-приложения, можно использовать React и Redux. Правда, потребуются вспомогательные библиотеки вроде react-redux и куча boilerplate-кода. А можно попробовать Hyperapp.


Hyperapp представляет собой единую библиотеку, которая обеспечивает управление состояним приложения (state) и иммутабельность, как в Redux/Elm, в сочетании с отображением представления (view) и Virtual DOM, как в React. Hyperapp использует подходы функционального программирования при управлении своим состоянием, но более гибко подходит к разрешению побочных эффектов (side effects), асинхронных действий и манипуляций с DOM. Hyperapp предоставляет мощную абстракцию для создания веб-приложений, но при этом дает вам полный доступ к нативным API, чтобы не ограничивать вас.


image


Код скажет больше, чем тысяча слов


Простое приложение-счетчик на React + Redux против эквивалента на Hyperapp:


React/Redux vs Hyperapp


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


1. Состояние (state)


В Redux состояние (state) может быть любого типа, хотя настоятельно рекомендуется выбирать такой тип данных, который легко сериализовать. Подавляющее большинство разработчиков в качестве начального состояния (state) для редюсера (reducer) использует пустой объект.


Не отражено в коде, но вызов Redux.createStore принимает в качестве необязательного аргумента начальное состояние (state). В Hyperapp состояние (state) всегда является объектом. Вместо двух разных способов инициализации состояния (state) здесь один.


2. Действия (actions)


В Redux генераторы действий (action creators) — это функции, которые возвращают действия (actions), как объекты JavaScript. Обычно генераторы действий (action creators) подключаются к Redux хранилищу (store) с помощью bindActionCreators, либо автоматически, либо вручную, используя аргумент mapDispatchToProps для ReactRedux.connect. Действия (actions) обычно определяются как множественный экспорт из одного файла, который затем втягивается в одно пространство имен, используя import * as actions from "./actions" при использовании модулей ES6.


В Hyperapp — генераторы действий (action creators), редюсеры (reducer) и bindActionCreators не нужны. Действия (actions) это чистые функции, которые иммутабельно меняют состояние (state) и имеют все данные необходимые для этого.


3. Изменение состояния (state)


В Redux изменение состояния (state) происходит в редюсере (reducer), который является чистой функцией, принимает состояние (state) и действие (action), возвращая следующее состояние (state). Действие (action) может обновить state (состояние) в любом редюсере (reducer).
Функция изменения состояния (state) имеет следующий вид:


(state, action) => nextState

Hyperapp использует функцию изменения состояния (state) такого вида:


(action) => (state [, actions]) => nextState

Не отражено в коде, но Hyperapp выполняет слияние (merge) состояния. Поэтому вместо Object.assign или {... state, key: "value"} достаточно просто return: {key: "value"}.


4. Представление (view)


В Redux представление (view) должно быть вручную подключено к состоянию (state) и генераторам действий (action creators). Для этого приходится использовать функцию высшего порядка (HOC) ReactRedux.connect, которая обертывает ваш компонент для подключения его к Redux хранилищу (store). Чтобы это работало, вы также должны обернуть свое приложение в <ReactRedux.Provider>, что делает ваше хранилище (store) доступным для любых компонентов, которые хотят подключиться к нему.


В Hyperapp ваше состояние (state) и действия (actions) автоматически подключаются к вашему представлению (view), и только компоненты верхнего уровня имеют к ним доступ.


Slices


Redux и Hyperapp поддерживают состояние с вложенными пространствами имен, однако они делают это, используя несколько разные подходы. В Hyperapp это идет из коробки — в Redux требуется вручную использовать combineReducers.


Код с использованием Redux:


const potatoReducer = (potatoState = initialPotatoes, action) => {
  switch (action.type) {
    case FRY:
    // ...
  }
}
const tomatoReducer = (tomatoState = initialTomatoes, action) => {
  switch (action.type) {
    case GRILL:
    // ...
  }
}
const rootReducer = combineReducers({
  potato: potatoReducer,
  tomato: tomatoReducer
})
// This would produce the following state object
{
  potato: {
    // ...potatoes
    // and other state managed by the potatoReducer... 
  },
  tomato: {
    // ...tomatoes
    // and other state managed by the tomatoReducer...
    // maybe some nice sauce?
  }
}

Эквивалентный код с использованием Hyperapp:


const rootState = {
  potato: {
    // ...just potato things
  },
  tomato: {
    // ...just tomato things
    // maybe some nice sauce?
  }
}
const rootActions = {
  potato: {
    // these actions receive only
    // the potato state slice and actions
  },
  tomato: {
    // these actions receive only
    // the tomato state slice and actions
  }
}

Async Actions


Пример организации асинхронных действий (acions)


const actions = {
  upLater: value => (state, actions) => {
    setTimeout(actions.up, 1000, value)
  },
  // Called one second after upLater
  up: value => state => ({ count: state.count + value })
}

Effects


image


import { withEffects, http } from "hyperapp-effects"
const state = {
  // ...
}
const actions = {
  foo: () => http("/data", "dataFetched"),
  dataFetched: data => {
    // data will have the JSON-decoded response from /data
  }
}
withEffects(app)(state, actions).foo()

Можно добавлять собственные эффекты.


Middleware


Для расширения возможностей генераторов действий (action creators) Redux предполагает использование applyMiddleware на уровне создания хранилища (store).


Hyperapp предполагает ручную композицию actions (действий) и middleware.


// Manual composition
hoa3(hoa2(hoa1(app)))(state, actions, view, document.body)

// Or with a standard-issue compose function
compose(hoa3, hoa2, hoa1)(app)(state, actions, view, document.body)

// Compose plays nicely with using different HOAs per environment
const hoas = NODE_ENV === "production" ? productionHoas : devHoas
compose(...hoas)(app)(state, actions, view, document.body)

Простым примером middleware является hyperapp-logger, который выводит информацию на консоль при вызове любого из ваших действий (actions):


image


logger(options)(app)(state, actions, view, document.body)

Завершение


Hyperapp воспринимает простоту так же серьезно, как и Redux. Сделать сложное простым, а большее меньшим возможно. Исходный код Hyperapp составляет ~300 строк кода, который я могу прочитать, когда у меня возникают вопросы, или при отладке, когда у меня есть проблемы. Размер библиотеки всего 1,4 кБ.


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

Комментарии (38)


  1. Dimensi
    26.02.2018 10:07

    Так, hyperapp для серьезных приложений же не подходит, или цель статьи в другом? Насколько я помню, даже автор либы не определился как сделать стейт в hyper app глобальным. Потому, что только рут компонент обладает состоянием, а все его дети только припасами.


    1. MrCheater Автор
      26.02.2018 10:42

      То, что только рут компонент является "умным", а все дочерние "глупыми" — нормально. Многие и на React/Redux так пишут


      1. Dimensi
        26.02.2018 10:44

        На все приложение один контейнер? Звучит сложновато.


        1. MrCheater Автор
          26.02.2018 10:46

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


      1. DarthVictor
        26.02.2018 17:22
        +1

        На хрена в таком варианте Redux? Чем он при таком подходе лучше this.setState()?


        1. ArtificialLife
          27.02.2018 00:19

          Redux — это ведь не только про стор (хранение данных). Его dev-tools сильно облегчают разработку, банальная для redux возможность экспортировать и импортировать стор чего только стоит. В случае ванильного this.setState() эту возможность придется реализовывать.

          Это первое, что пришло в голову.


  1. justboris
    26.02.2018 11:02

    Очень однобокое получилось голосование. Кроме вариантов "за redux" и "за hyperapp" есть куча других, начиная от ванильного this.setState(), и заканчивая другими библиотеками, типа mobx или unstated.


    1. Laney1
      26.02.2018 12:25

      кстати для mobx еще сделали mobx-state-tree — мутабельный контейнер, умеющий прикидываться стором redux. Например, его можно дебажить с помощью redux-devtools. По-моему крутой проект


      1. mayorovp
        26.02.2018 13:14

        А по-моему, не очень. Потому что в нем потеряли статическую типизацию и синтаксис классов…


        1. farwayer
          26.02.2018 17:20

          Второе решаемо. Может у меня дойдут руки оформить в либу набор декораторов для этого. Статическая типизация — это о чем?


      1. farwayer
        26.02.2018 17:22

        Идея интересная, но пока сыровато.


    1. Carduelis
      26.02.2018 12:27

      Насколько mobx уменьшает боль? Я читал и смотрел презентации от яндекса, но на сайте mobx пугают говорится, что это библиотека, а не фреймворк, и что это не замена редаксу. Слышал, что нужен mobx-state-tree.
      Какой набор аддонов к mobx нужно добавить, чтобы заменить стандартные миддлверы редакса? (да, идея redux-thunk, вроде как из коробки работает в mobx)


      1. mayorovp
        26.02.2018 13:15

        Это как раз-таки замена редаксу. А какие миддлверы вам тут нужны? А то я от redux убежал едва увидев примеры, и про стандартные мидлверы ничего не знаю (и, наверное, я тут не один такой).


      1. Akuma
        26.02.2018 17:14
        +1

        А зачем вам идея идея redux-thunk в mobx?

        В mobx вы просто меняете объекты как обычно в JS и не паритесь на счет остального. В этом главная фишка. Можно вообще не использовать action, если у вас что-то простое, но в то же время action-ом может быть любая JS функция или метод любого «левого» класса. Т.е. даже банальный $.get('/url', () => {store.object.title = 'some value';}) будет работать на-ура.

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


      1. farwayer
        26.02.2018 17:16
        +1

        По сравнению с классическим redux cильно уменьшает. Правда, появляется соблазн пихать всю логику прямо в сторы. На мелких проектах так норм, а на крупных без выделение отдельного бизнес-слоя скоро начинается треш.


    1. untilx
      26.02.2018 13:54

      Однозначно не хватает варианта «против всех»


  1. bgnx
    26.02.2018 12:25

    Интересно где у этого фреймворка mapStateToProps или хотя бы возможность сделать вложенные actions? На приложениях вроде счетчика или тодошек все фреймворки выглядят сносно но вот ближе к реальности начинаются ограничения и костыли. В данном случае допустим мы хотим добавить фичу вложенных тодошек, чтобы юзер мог разбить большую подзадачу на мелкие а те в свою очередь на более мелкие и не нужно ограничивать юзера в количестве уровней. Как эта задача решается в hyperapp? Если идти по пути вложенности (хранить в объекте todo массив вложенных тодошек) то как обновить текст какой-то тодошки на n-уровне вложенности? Нам нужно вернуть новый объект состояния и нужно как-то рекурсивно пересоздать объекты и массивы всех родителей и неясно как это решается hyperapp. А если идти по пути нормализации, когда в тодошке храним не объекты а айдишники а сами объекты в плоском хеше объектов по их айдишнику то нужно добавить аналог mapStateToProps чтобы изменения одной тодошки не вызвало перерендер всего списка тодошек или вообще всего приложения


  1. staticlab
    26.02.2018 13:00

    более гибко подходит к разрешению побочных эффектов (side effects), асинхронных действий и манипуляций с DOM

    Вот надо было как раз эту тему и раскрыть. Простой +1 / -1 можно на любом фреймворке легко сделать.


    1. MrCheater Автор
      26.02.2018 14:26

      Fixed


  1. Gentlee
    26.02.2018 13:00

    Как тут дела с асинхронными действиями?

    Мне сразу redux + react-redux показались слишком усложненными и я быстро накатал вот такой вариант, без боилерплейта. В продакшене уже в двух проектах.


    1. MrCheater Автор
      26.02.2018 14:19

      Как-то так:


      const actions = {
        upLater: value => (state, actions) => {
          setTimeout(actions.up, 1000, value)
        },
        // Called one second after upLater
        up: value => state => ({ count: state.count + value })
      }

      P.S. — добавил пример в текст статьи


      1. staticlab
        26.02.2018 15:45
        +1

        Поясните, пожалуйста, как работает этот код? actions.up будет содержать функцию-обёртку над функцией up, непосредственно изменяющую стейт?


        1. MrCheater Автор
          26.02.2018 15:54

          1. Интерфейс дергает upLater
          2. upLater ждет секунду и дергает up
          3. up меняет state
          4. Интерфейс обновляется


          1. staticlab
            26.02.2018 16:00

            Вот это и непонятно. setTimeout дёргает оригинальный up(value), который просто вернёт функцию state => (newState)? Или параметр actions это тот же, который передаётся в функцию view?


            1. MrCheater Автор
              26.02.2018 16:06
              -1

              По-идее — в объекте actions будут лежать обертки, которые не требуют ручной передачи state


            1. mayorovp
              26.02.2018 16:20
              +1

              Да, вызывается этот метод вот так:


                          actions[key] = function(data) {
                            if (typeof (data = action(data)) === "function") {
                              data = data(get(path, globalState), actions)
                            }

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


      1. Gentlee
        26.02.2018 16:16

        Если после вызова actions.up в upLater мне понадобиться узнать текущее состояние, я не смогу это сделать? ведь тогда нужен метод getState, а не просто state.


        1. mayorovp
          26.02.2018 16:21

          После вызова actions.up — это вообще как? actions.up как бы вызывается через секунду после того как upLater закончил работу.


  1. bakhirev
    26.02.2018 13:01

    -


  1. Miraage
    26.02.2018 15:12

    Как я понял, они пытались сделать клон Cerebral (который, кстати, уже юзают в продакшне весьма крупные проекты, типа CodeSandbox).


  1. Telman
    26.02.2018 15:17

    Спасибо


  1. Fen1kz
    26.02.2018 18:22
    +1

    Не нашел плюсов кроме «это у нас из коробки, это тут тоже автоматом и это из коробки». Если добавилась только изкоробочность, почему просто не сделать надстройку над R/R?

    Да и придирки к бойлерплейту какие-то… Странные. Ну как вам не написать / взять один раз сделать вес обвес автоматикой и дальше просто писать чистые редьюсеры в стиле const handleAction = (state, {data}) => newState?


    1. MrCheater Автор
      26.02.2018 18:42

      Хочу обратить внимание, что я не автор. А это перевод.
      Но с React/Redux работаю давно. И архитектура React не позволит некоторые "упрощения" hyperapp реализовать. Например, автоматический биндинг action creators сделать не получится без каких-то серьезных проломов и глобальных переменных. Максимум, что-то похожее можно отыскать тут redux-actions.
      А в остальном вы правы, что-то похожее можно и на React/Redux изобразить, если приложить усилий. Думаю, в hyperapp упор именно на "изкоробочность".


    1. Grammka
      27.02.2018 00:05
      +1

      Гляньте плз на такое решение (обертку над R/R) Redaction


  1. Mycolaos
    26.02.2018 21:06

    Код скажет больше, чем тысяча слов*

    Ну вот, на первом же примере обломался. Смотришь картинку, и видишь, что вариант Хайпераппа точь также делаешь на Реакте без Редакса.

    Дальше читать перехотелось.

    *В данном случае код говорит о том, что тебе лапшу на уши вешают.


    1. zapolnoch
      27.02.2018 23:12

      Мне тоже больше напомнило обычный React + recompose


  1. n0ne
    01.03.2018 11:11

    И как обычно: а что это за тема и шрифт? ((((-:


  1. PaulMaly
    01.03.2018 17:48

    Hyperapp вроде тормоз знатный, не?