Хабрахабр

[Перевод] Используем Ramda вместе с Redux

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

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

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

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

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

React

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

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

Redux

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

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

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

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

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

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

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

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

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

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

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

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

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

{ 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'
}

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

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

evolve(__, state)({ board: update(index, state.nextToken), nextToken
} function nextToken(token) { return token === 'X' ? 'O' : 'X'
}

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

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

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

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

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

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'])

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

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

В секции “сокращаем заготовку” документации к 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)
}

Заключение

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

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть