Это перевод статьи Using Ramda With Redux, в которой рассказывается о том, как упростить ваш код на основе библиотеки Redux с помощью библиотеки Ramda, позволяющей писать код в функциональном стиле.


P.S. Если вы не знаете, что такое Ramda — приглашаю вас к переводу цикла статей о ней.


На моей текущей работе мы работаем над фронтенд-проектами, использующими React и Redux. Мы также используем библиотеку Ramda для того чтобы эффективно работать с Redux. Данный пост описывает несколько способов, в которых мы использовали Ramda в наших React/Redux приложениях.


Предпоссылки


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


React


React — это "JavaScript библиотека для создания пользовательских интерфейсов". Данный пост не является руководством к React, и многое из того, о чём я буду здесь говорить, не зависит от React. Если у вас есть желание разобраться с реактом — можно начать со статьи Пете Хантса "Мышление в стиле React".


Для данного поста, главная вещь, которую нужно знать — это то, что React подчёркивает методику декомпозиции интерфейса на дерево компонентов. Каждый компонент получает "свойства" (которые часто называют "пропсами"), которые конфигурируют этот компонент для текущего контекста. Компонент может также иметь некоторое внутреннее "состояние", которое может меняться.


Redux


Redux — это "предсказуемый контейнер состояния для JavaScript приложений". Redux имплементирует что-то похожее на архитектуру Flux, используя идеи из Elm. Будучи не привязанным к React, Redux часто используется вместе с ним.


Данный пост также не является руководством к Redux; чтобы разобраться в нём, можно посмотреть серию видео-уроков от Дэна Абрамова на egghead.io. Даже если вы не думаете, что когда-нибудь будете использовать Redux, стоит посмотреть эти видеоролики просто для того чтобы узнать, как эффективно преподавать материал. Учебный поток этих видео потрясающий.


Базовая архитектура Redux приложений состоит из одного объекта, содержащего всё состояние (state) приложения. Текущее состояние содержится в "хранилище" (store). Пользовательский интерфейс рендерится как чистая функция, использующая это состояние, что прекрасно подходит для React. Состояние никогда не изменяется напрямую; вместо этого, пользовательские экшены и/или асинхронные действия создают экшены, которые, в свою очередь, вызываются через хранилище. Хранилище передаёт текущее состояние и экшен в "редюсер", который возвращает новое состояние хранилища. Хранилище заменяет это состояние новым и обновляет пользовательский интерфейс, завершая на этом весь цикл.


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


При использовании вместе с React, почти всё состояние, которое может храниться в индивидуальных React компонентах, — перемещается в хранилище. Большинство React компонентов в Redux приложении используют только пропсы для выполнения своей работы.


Важной частью архитектуры для данного поста является редюсер, который определяется как чистая функция из (currentState, action) -> newState, и отображает состояние компонентов интерфейса. В Redux с React, эта последняя работа выполняется чистой функцией mapStateToProps(state, ownProps) -> extraProps. ownProps это свойства, которые используются для создания компонента, и extraProps — это дополнительные свойства, которые будут переданы компоненту вместе с ownProps.


Ramda


Ramda называет себя "практичной функциональной библиотекой для JavaScript программистов". Она предоставляет множество возможностей, которые используют функциональные программисты. В отличие от чего-то вроде Immutable, она работает с чистыми JavaScript объектами.


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


Давайте взглянем на некоторые способы, с которыми мы можем использовать Ramda, когда мы пишем свой Redux код.


Написание редюсеров


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


Обновление свойств объектов


В документации к Redux есть пример todo-приложения. Один из редюсеров содержит данный сниппет кода:


Object.assign({}, state, {
  completed: !state.completed
})

Если вы используете Babel и его синтаксис расширения объектов, вы можете писать это немного более лаконично, вот так например:


{
  ...state,
  completed: !state.completed
}

Есть несколько способов написать подобное на Ramda. Вот довольно прямой порт:


assoc('completed', !state.completed, state)

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


Другой способ написания этого — использование функции evolve:


evolve({
  completed: not
}, state)

evolve берёт объект, который описывает функцию трансформации для каждого ключа. В данном случае мы указываем, что значение свойства completed должно быть трансформировано функцией not.


Используя evolve, вы можете компактно указать множество трансформаций своего состояния.


Когда я использую evolve, я склонен идти дальше и использовать __ для того чтобы сделать его чуть более ясным для чтения:


evolve(__, state)({
  completed: not
})

Для преобразования таких простых вещей, я более склонен использовать стандартный синтаксис расширения объектов из ES7. Но если ваши редюсеры становятся немного более сложными, использование Ramda сильно упрощает код, как мы сможем увидеть далее.


Обновление массива элементов


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


{
  board: ['X', 'O', 'X', ' ', 'O', ' ', ' ', 'X', ' '],
  nextToken: 'O'
}

Мы имеем экшен PLACE_TOKEN, который включает в себя индекс, указывающий, куда следует установить следующий символ.


Используя синтаксис расширения массивов из ES6 и синтаксис расширения объектов из ES7, мы можем написать наш редюсер для этого в следующем виде, после извлечения маленькой функции-хелпера nextToken:


{
  ...state,
  board: [
    ...state.board.slice(0, index),
    state.nextToken,
    ...state.board.slice(index + 1)
  ],
  nextToken: nextToken(state.nextToken)
}

function nextToken(token) {
  return token === 'X' ? 'O' : 'X'
}

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


Для того чтобы упростить данный код, мы можем использовать Ramda функцию update для обновления нашей доски:


{
  ...state,
  board: update(index, state.nextToken, state.board),
  nextToken: nextToken(state.nextToken)
}

function nextToken(token) {
  return token === 'X' ? 'O' : 'X'
}

update предоставляет нам новый массив, который обновляет элемент по индексу index указанным значением. Если вам нужно трансформировать существующий элемент массива вместо замены его, вы можете использовать функцию adjust для этого.


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


evolve(__, state)({
  board: update(index, state.nextToken),
  nextToken
}

function nextToken(token) {
  return token === 'X' ? 'O' : 'X'
}

Если вы не знакомы с ES6, { nextToken } — это короткая запись { nextToken: nextToken }.


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


В данном случае, evolve будет вызывать нашу каррированную функцию update с оригинальным значением state.board, также как мы вызываем нашу функцию nextToken с оригинальным значением state.nextToken.


Добавляем State в Props


Ramda также может быть удобна при добавлении состояния в пропсы компонента. В приложении крестиков-ноликов, компонент Board нуждался в элементах board и nextToken из состояния. Традиционный путь написания этого следующий:


function mapStateToProps(state) {
  return {
    board: state.board,
    nextToken: state.nextToken
  }
}

Это можно упростить с помощью Ramda-функции pick:


function mapStateToProps(state) {
  return pick(['board', 'nextToken'], state)
}

Обратите внимание, что мы прокидываем state в нашу функцию, и далее прокидываем её в pick. Когда мы видим подобный паттерн, это ключ к тому, что мы можем использовать преимущество каррирования. Давайте применим его здесь:


const mapStateToProps = pick(['board', 'nextToken'])

Здесь вновь, мы только предоставляем один из двух аргументов, в которых нуждается pick, так что мы получим взамен каррированную функцию, которая будет ожидать переменную state. И это произойдёт тогда, когда Redux вызовет эту функцию для нас.


Создание редюсеров


В секции “сокращаем заготовку” документации к Redux предлагается, как возможно написать функцию createReducer, которая бы принимала initialState и объект, содержащий обработчики состояния:


function createReducer(initialState, handlers) {
  return function reducer(state = initialState, action) {
    if (handlers.hasOwnProperty(action.type)) {
      return handlers[action.type](state, action)
    } else {
      return state
    }
  }
}

Используя функции Ramda propOr и identity, мы можем немного упростить тело функции редюсера:


function createReducer(initialState, handlers) {
  return function reducer(state = initialState, action) {
    propOr(identity, action.type, handlers)(state, action)
  }
}

propOr принимает значение по умолчанию, название свойства и объект. Если объект имеет совпадающее по имени свойство, значение этого свойства будет возвращено. Иначе будет возвращено значение по умолчанию. Начиная с этого, мы можем получить обратно функцию для вызова состояния и экшена. Если эта функцию идентична, она просто вернёт первый параметр, который является состоянием. Так что получается, что данный код будет делать точно то же самое, что и вышенаписанный изначальный код без Ramda.


Мы можем также использовать стрелочную функцию из ES6 для ещё большего упрощения этого:


function createReducer(initialState, handlers) {
  return (state = initialState, action) =>
    propOr(identity, action.type, handlers)(state, action)
}

Заключение


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


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


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


Благодарности


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

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


  1. Acionyx
    26.08.2018 17:14

    Отвратительный перевод. Вы его хоть читали перед публикацией сюда?


    1. saggid Автор
      26.08.2018 17:18

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


      1. Acionyx
        26.08.2018 18:10

        Похвальное желание работать над ошибками


        1. saggid Автор
          26.08.2018 18:24

          Всегда есть над чем поработать, но я повторяюсь: перевод писался не для таких как вы. Такие как вы, идеально знающие английский, всегда могут сесть и написать свою идеальную вариацию и сидеть и спокойно радоваться ей. А мой перевод просто не читайте.


          1. Acionyx
            27.08.2018 00:40

            Дело вообще не в английском, а в вычитке русского текста. Ни в одной русскоязычной литературе вы не встретите таких формулировок, это просто не по-русски:

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


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


            1. saggid Автор
              27.08.2018 00:49

              Ох Господи. Сорян, это я сейчас поправлю. Вообще у меня сильно болит голова, и я реально старался и даже прочёл всё дополнительно перед публикацией, но каким-то образом этот отрывок показался мне нормальным)


              1. modestguy
                27.08.2018 08:29

                и я реально старался

                не Вы, а google translate )


                1. saggid Автор
                  27.08.2018 10:37

                  Господи, как всегда одни гении в комментариях, которые уже всё знают. Что-нибудь полезное кто-нибудь из ваш напишет сюда?


                1. saggid Автор
                  27.08.2018 10:47

                  Можно пойти и проверить как это абзац переводит гугл: "Я еще не проработал много функциональных программ, но я считаю, что Ramda является довольно доступным способом проникнуть в нее, и это действительно похоже на стиль, который рекомендует Redux."


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