Hyperapp для беженцев с React/Redux

Автор оригинала: Wolfgang Wedemeyer
  • Перевод

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.

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

React/Redux vs Hyperapp

  • 40.1%React + Redux = ❤90
  • 59.8%Я устал писать тонны boilerplate кода на каждый чих в 10 разных местах134
Developer Soft
75,15
Компания
Поделиться публикацией

Похожие публикации

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

    0

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

      0

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

        0

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

          0

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

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

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

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

        • НЛО прилетело и опубликовало эту надпись здесь
            0
            А по-моему, не очень. Потому что в нем потеряли статическую типизацию и синтаксис классов…
              0
              Второе решаемо. Может у меня дойдут руки оформить в либу набор декораторов для этого. Статическая типизация — это о чем?
              0
              Идея интересная, но пока сыровато.
              0
              Насколько mobx уменьшает боль? Я читал и смотрел презентации от яндекса, но на сайте mobx пугают говорится, что это библиотека, а не фреймворк, и что это не замена редаксу. Слышал, что нужен mobx-state-tree.
              Какой набор аддонов к mobx нужно добавить, чтобы заменить стандартные миддлверы редакса? (да, идея redux-thunk, вроде как из коробки работает в mobx)
                0
                Это как раз-таки замена редаксу. А какие миддлверы вам тут нужны? А то я от redux убежал едва увидев примеры, и про стандартные мидлверы ничего не знаю (и, наверное, я тут не один такой).
                  +4
                  А зачем вам идея идея redux-thunk в mobx?

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

                  Я на mobx с редукса сбежал просто неглядя. Сейчас redux кажется мне адовой смесью бредовых идей его автора (без обид).
                    +2
                    По сравнению с классическим redux cильно уменьшает. Правда, появляется соблазн пихать всю логику прямо в сторы. На мелких проектах так норм, а на крупных без выделение отдельного бизнес-слоя скоро начинается треш.
                    0
                    Однозначно не хватает варианта «против всех»
                    +1
                    Интересно где у этого фреймворка mapStateToProps или хотя бы возможность сделать вложенные actions? На приложениях вроде счетчика или тодошек все фреймворки выглядят сносно но вот ближе к реальности начинаются ограничения и костыли. В данном случае допустим мы хотим добавить фичу вложенных тодошек, чтобы юзер мог разбить большую подзадачу на мелкие а те в свою очередь на более мелкие и не нужно ограничивать юзера в количестве уровней. Как эта задача решается в hyperapp? Если идти по пути вложенности (хранить в объекте todo массив вложенных тодошек) то как обновить текст какой-то тодошки на n-уровне вложенности? Нам нужно вернуть новый объект состояния и нужно как-то рекурсивно пересоздать объекты и массивы всех родителей и неясно как это решается hyperapp. А если идти по пути нормализации, когда в тодошке храним не объекты а айдишники а сами объекты в плоском хеше объектов по их айдишнику то нужно добавить аналог mapStateToProps чтобы изменения одной тодошки не вызвало перерендер всего списка тодошек или вообще всего приложения
                      0
                      более гибко подходит к разрешению побочных эффектов (side effects), асинхронных действий и манипуляций с DOM

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

                    • НЛО прилетело и опубликовало эту надпись здесь
                        +2

                        Как-то так:


                        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

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

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

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

                                0

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

                                  +1

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


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

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

                            • НЛО прилетело и опубликовало эту надпись здесь
                                0

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

                            0
                            -
                              0
                              Как я понял, они пытались сделать клон Cerebral (который, кстати, уже юзают в продакшне весьма крупные проекты, типа CodeSandbox).
                                0
                                Спасибо
                                  0
                                  Не нашел плюсов кроме «это у нас из коробки, это тут тоже автоматом и это из коробки». Если добавилась только изкоробочность, почему просто не сделать надстройку над R/R?

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

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

                                      +1
                                      Гляньте плз на такое решение (обертку над R/R) Redaction
                                      0
                                      Код скажет больше, чем тысяча слов*

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

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

                                      *В данном случае код говорит о том, что тебе лапшу на уши вешают.
                                        0
                                        Мне тоже больше напомнило обычный React + recompose
                                        0
                                        И как обычно: а что это за тема и шрифт? ((((-:
                                          0
                                          Hyperapp вроде тормоз знатный, не?
                                            0
                                            Что бы не писать тонны boilerplate кода в связке react + redux можно использовать github.com/pauldijou/redux-act, использование данной либы сокращает количество кода в разы.

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

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