Немного Ramda для React и Redux
С последней моей записи прошло довольно много времени. Дело в том, что я все же нашел свою первую работу и теперь времени на сторонние проекты у меня стало меньше. Тем не менее, постараюсь более менее регулярно продолжать писать на интересные для меня (надеюсь, и для вас) темы.
Я давно хотел попробовать что-то из мира функционального программирования. Если для библиотек вроде Immutable.js я не нашел пока места при решении своих повседневных задач, то вот Ramda для меня оказалась очень удобной.
Ramda для управления логикой приложения
Оператор pipe, а также ifElse, cond, andThen / otherwise, позволяют создавать краткие и выразительные функции, состоящие из более простых частей.
Например, нам требуется функция, которая бы делала GET запрос на сервер и получала бы массив каких-либо данных (пользователи, товары, продажи и т.д.) - назовем их entities. При этом требуется проверить, что статус ответа равен 200.
Функция GET запроса выглядит так:
Тогда целиком функция, отвечающая за получение и проверку данных с сервера, будет иметь вид:
Последняя строка в ней является лишней, так как axios запрос не должен пробрасывать ошибки, но я добавил здесь на всякий случай, а также в целях демонстрации.
- Функция R.pipe последовательно выполняет функции, переданные в нее в качестве аргументов.
- R.always(entity) - функция, ничего не делает, просто возвращает переменную entity (так как требуется именно функция). Это аналог записи () => entity.
- fetchData принимает ответ с предыдущей функции - entity в качестве своего аргумента и делает GET запрос на сервер.
- R.andThen и R.otherwise - аналог записи .then и .catch. В случае, если произошла ошибка, то R.otherwise вызывает функцию, пробрасывающую ошибку.
- R.ifElse принимает 3 функции в качестве аргументов: первая функция должна вернуть true или false, исходя из чего запускается 2-ая или 3-я функции. Запись (data) => ...: здесь в качестве аргумента принимается ответ от предыдущей функции fetchData.
- R.pick(['data']) является последней функцией в списке. Она получает объект и возвращает значение по ключу. В данном случае - это data, где должен содержаться интересующий нас массив.
В чем преимущества подобной записи функции для меня?
Во-первых, запись получается очень краткой и с первого взгляда можно легко понять, что здесь происходит.
Во-вторых, ее довольно легко расширять, добавляя новые функции в любое место среди аргументов R.pipe.
На данном примере преимущества могут быть не так заметны. Но, если запрос требует более сложной логики и большего числа манипуляций с данными, то использование функции pipe может оказаться заметно предпочтительнее.
Ramda для Redux Slices
Рекомендуемым инструментом при использовании Redux является библиотека @reduxjs/toolkit, которая берет на себя часть рутинных действий по подготовке к работе.
Использование Redux Toolkit работу действительно проще. Но, лично у меня, дискомфорт вызывает использование библиотеки Immer для изменения state хранилища. Immer оборачивает state в proxy и, соответственно, менять так, будто нас не интересует иммутабельность. Все мутации будут перехвачены на уровне прокси и на выходе мы получим новый state, старый при этом никак изменен не будет.
Например, если при запросе на сервер нам нужно поменять fetchingStatus на pending, то это может выглядеть так:
Мне такой подход кажется не совсем удачным:
- Код получается не таким, каким хотелось бы его видеть. А именно, без мутаций state, причем в Redux.
- Использование переменной state вводит в заблуждение. Уместнее было бы называть ее stateProxy, чтобы избежать возможной путаницы.
Пока что я остановился на таком подходе:
- получаю state из прокси с помощью функции current из библиотеки @reduxjs/toolkit
- на его основе получаю новый state, который и возвращаю (то есть, работаю с ним "по-старинке")
И в этом опять же помогает Ramda за счет своей затаенностью на иммутабельность данных и удобные инструменты для работы с объектами.
Приведенный выше пример выглядит так:
Он получился более многословным, чем предыдущий вариант, но, при этом, и более однозначным.
Вот пример кода, когда нам нужно загрузить дополнительные данные, к уже существующим:
В дальнейшем попробую найти для Ramda более широкое применение и попробовать библиотеку fp-ts, которая вроде аналогичная, но имеет более качественную типизацию для работы с Typescript (в некоторых случаях, к сожалению, с Ramda не всегда удается обойтись без as).