Pull to refresh
97.57
SimbirSoft
Лидер в разработке современных ИТ-решений на заказ

Личный опыт: переход с Redux на Effector. И при чем тут DX

Level of difficultyMedium
Reading time6 min
Views6K

Frontend-разработка очень богата различными инструментами. Новые фреймворки и библиотеки выходят чуть ли не каждый день и, к сожалению, не все из них одинаково полезны или могут сделать ваш продукт лучше. Кроме того, они различаются по степени удобства именно для разработчика. Есть такое понятие DX – Developer eXperience – по аналогии с UX. Это то, насколько разработчику удобно, интуитивно понятно пользоваться определенным сервисом.

Меня зовут Аня, я frontend-специалист в компании SimbirSoft с опытом в разработке более трех лет. Уже успела поработать со многими инструментами, участвовала в проекте, где переносили огромное приложение на новые библиотеки, в том числе заменяли Redux на Effector. В этой статье хочу поделиться своими мыслями об этих стейтменеджерах с точки зрения DX. 

Да, их сравнивали много раз, но мой акцент будет на том, как писать код на Effector для привычных кейсов в Redux. Подчеркну, DX — это не про рациональные аргументы, а про комфорт, фэншуй и тому подобные вещи (вы же понимаете, о чем я, правда?…).

Забегая вперед, хочу сказать, что Effector мне понравился. И прежде всего своей простотой — да-да, один из наших любимых принципов KISS). И может быть, что я по поводу Effector испытываю ещё какую-то национальную гордость, потому что это разработка ребят из России.

В чём преимущества

Следующая иллюстрация – моя попытка визуализировать использование стейтменеджера в приложении. На страницу выводится имя пользователя, в коде это переменная name, значение которой получают по запросу.

Мы можем видеть, что Redux создает общий store. Для него объединяются все редьюсеры из разных частей проекта в общий rootReducer. А также обеспечивается ожидание выполнения асинхронных запросов, за которые отвечает отдельная библиотека redux-saga. Саги со всего проекта объединяют в общую rootSaga и передают в store.

Наверное, у всех такое было — разрабатываешь новую фичу, делаешь все как написано, а не работает. Полдня ищешь ошибку, ругаешься незлым тихим матом, а потом — ах, батюшки, просто не прокинул сагу наверх. Мало того, что лишний код, сколько лишнего времени и работы.

Никто не станет спорить, что в плане бойлерплейт Redux очень показателен. До сих пор многие «возрастные» проекты содержат части на классовых компонентах React, где store написан на классическом Redux, даже без toolkit. Вспомните сколько там папок (reducers, actions, selectors, sagas…) и сколько файлов в каждой папке.

А как здорово дебажить такие проекты: пропсы передаются через connect, то есть кликнув в IDE по имени пропса или функции, до саги или редьюсера не доберешься. С этим же связана проблема анализа проекта на «мертвый код». Саги никуда не импортируются, поэтому невозможно понять, что используется, а что нет с помощью инструментов IDE типа find file references.

Итак, чем хорош Effector:

  1. Для Effector нет необходимости писать общий стор, достаточно описать модель в конкретной части проекта. Файлы хранилища используют обычные способы импорта-экспорта.

  1. Кроме того, создатели Effector подчеркивают его декларативность — то есть вам не надо расписывать порядок действий, а надо сказать Effector что вы хотите, а он сделает (меньше букв, удаление «бойлерплейт»).

  1. Третье заявленное преимущество — снижение «болей» разработчиков, — Effector понятнее и легче в использовании. Подписываюсь.

Далее поговорим о том, как использовать Effector. В статье я попыталась рассказать об этом максимально простыми словами с примерами, но еще настоятельно рекомендую взглянуть на официальную документацию.

Как использовать Effector в работе

Юниты

Данные храним в store, создаем с помощью функции createStore(начальное значение), имя принято начинать с $:

const $name = createStore('');

Для синхронных действий с данными используем event:

const setName = createEvent();

Для асинхронных действий – effect, с помощью функции createEffect(async_function), имя принято заканчивать на Fx:

const getNameFx = createEffect(fetchName);

Effect позволяет описать обработку возможных исходов асинхронного действия с помощью методов .fail и .done (возвращают результат выполнения асинхронной функции – неудачный и удачный соответственно), есть также .doneData и .failData (возвращает поле data из response). Кроме того, состояние ожидания выполнения доступно из свойства .pending.

Store, event и effect – это юниты – основные сущности Effector. Есть еще четвертая – domain, она позволяет делать разные штуки со всеми данными в приложении одновременно (тестировать, например).

Связки

Когда сущности созданы, надо указать, как они связаны между собой. Для этого Effector предлагает много разных способов. Один из них — связки. Связки бывают разные, моя любимая sample. Главные поля: 

  • clock – срабатывание чего

  • target – вызывает что

В clock и target указываем event (событие), но если укажете store, его изменение будет восприниматься как event, а для effect все 4 метода .fail, .done, .failData и .doneData являются event.

sample({
	clock: setName,
	target: $name
}) //когда сработает setName, данные будут записаны в $name

Можно указывать массивы событий в обоих полях:

sample({
	clock: [setName, getNameFx.doneData],
	target: $name
}) //когда сработает setName или getNameFx.doneData, данные будут записаны в $name

Можно добавить обработку данных, которые получены из clock перед передачей в target:

sample({
	clock: [setName, getNameFx.doneData],
	fn: name => trim(name), 
	target: $name
})//когда сработает setName или getNameFx.doneData, данные будут обработаны функцией trim и записаны в $name

Часто с бэка приходят массивы данных, элементы которых не содержат поля id или key, которые так нужны реакту или, например, готовым компонентам ant-design. Самое место добавить обработку здесь:

sample({
	clock: getDataFx.doneData,
	fn: data => data.map((item) => {...item, key: uuid()})
	target: $data
})

Можно запускать target по условию:

sample({
	clock: [setName, getNameFx.doneData],
	filter: name => name !== ‘Василий’,
	target: $name
}) //когда сработает setName или getNameFx.doneData, данные будут проверены и, если имя не равно «Василий», записаны в $name

Можно указать дополнительный источник данных для обработки:

sample({
		clock: [setName, getNameFx.doneData],
		filter: name => name !== ‘Василий’,
		source: $lastName,
		fn: (lastName, name) => trim(name) + « » + trim(lastName),
		target: $name
	  })//когда сработает setName или getNameFx.doneData, данные будут проверены и, если имя не равно «Василий», обработаны с помощью функции fn, которая в качестве аргументов примет данные исходного события, а также store, указанного в поле source, результат обработки будет записан в $name

Есть рекомендация – для описания одного хранилища писать два файла:

  • в одном описываете и экспортируете юниты,

  • а в другом описываете «поток данных», в этом файле будут только импорты юнитов и никаких экспортов.

Кстати, хочу обратить ваше внимание, что в Effector много способов делать одни и те же вещи. Например, уже указанный кейс:

sample({
	clock: getNameFx.doneData,
	target: $name
}) 

Можно описать по-другому:

const getNameFx = createEffect(fetchName);
const $name = createStore(' ').on(getNameFx.doneData, (v) => v);

Или так:

const $name = createStore(' ');
const getNameFx = createEffect(fetchName);
getNameFx.doneData.watch((data) => $name.setState(data));

Типизация

В sample должны сочетаться типы clock и target (clock должен возвращать то, что нужно target). Указываем типы при создании юнитов:

const $name = createStore<string>('');
const setName = createEvent<string>();

Посложнее с effect:

Соответственно, если clock возвращает не тот тип, который нужен target, то fn должна возвращать тип для target. 

Гейты

Еще одна вещь, которую я часто использовала — Gate.

Часто компонент, который использует данные, полученные из запроса, использует конструкцию:

useEffect({
		dispatch(setNameAction())
	  }, [])

Я написала dispatch(action()), как положено в Redux.
С Effector можно написать так же:

useEffect({
		getNameFx()
	  }, [])

А можно использовать хук useGate или компонент Gate. В модели мы его создадим:

const NameGate = createGate();
forward({from: NameGate.state, to: getNameFx})

А в компоненте вызовем с помощью компонента <NameGate />. Работать будет как useEffect. Если у компонента NameGate будут пропсы, то родитель будет обновляться при изменении этих пропсов.

Кроме useEffect можно убрать все useState (перенести их в модель как пару store и event), и получается, всю работу с данными доверить Effector, это вроде тоже хорошо с точки зрения разделения ответственности.

Вместо вывода

На моём проекте для новой версии продукта выбрали Effector. Основными причинами для перехода с Redux были:

  • сокращение кодовой базы

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

Вообще в инструментах разработчиков есть две противоположные проблемы:

1. Если технология сильно декларативная (писать надо «мало букв», все под капотом), то разработчик перестает понимать как все устроено внутри, теряет корни, бедолага. Это как раз про Effector.

2. Если, наоборот, технология более низкоуровневая, надо понимать, что зачем, и прописывать многие детали — понимания больше, но больше и усталости, требуется больше внимания. Тяжело разработчику. А это Redux.

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

  1. Нужно писать надо гораздо меньше кода.

  2. Интуитивно понятно, как управлять данными — создавать переменные и изменять их значения, будь то синхронно или асинхронно.

  3. Нет необходимости создавать общее хранилище для всего приложения и учитывать его наличие при дальнейшей разработке; проще «дебажить»  существующий код и исследовать зависимости (что откуда приходит).

А причем тут DX? Еще раз повторю, что самое главное словами описать трудно, но лично у меня при использовании Effector “Developer eXperience is much better”:) 

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

Спасибо за внимание!

Больше авторских материалов для frontend-разработчиков от моих коллег читайте в соцсетях SimbirSoft – ВКонтакте и Telegram.

Tags:
Hubs:
Total votes 9: ↑7 and ↓2+5
Comments43

Articles

Information

Website
www.simbirsoft.com
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия