Универсальный протокол для переиспользования концепта useUnit
из биндингов под разные UI-фреймворки для библиотек из экосистемы.
В экосистеме Эффектора есть библиотеки, которые вводят свои кастомные сущности: Query из @farfetched/core
, Route из atomic-router
или modelFactory
из effector-factorio
.
Эти кастомные сущности представляют собой некий объект, основные свойства которого являются юнитами эффектора.
Сейчас каждая из этих библиотек заводит свои собственные библиотеки биндингов к UI-фреймворкам, многие из которых заключаются в очень простых надстройках над биндингами эффектора, например:
// farfetched/react/src/use_query.ts
import { useUnit } from 'effector-react';
function useQuery(query: Query) {
const [data, stale, error, pending, start] = useUnit([
query.$data,
query.$stale,
query.$error,
query.$pending,
query.start,
]);
return { data, stale, error, pending, start };
}
export { useQuery };
т.е. простой маппинг свойств объекта Query через useUnit
.
При этом сам useUnit
, как концепция, становится стандартом биндингов в экосистеме - он уже реализован в effector-react
, effector-solid
и запланирован в effector-vue
- во всех местах useUnit
ведет себя одинаковым образом:
- Принимает один юинт или произвольный шейп из нескольких юнитов
- Для сторов возвращает подходящий для фреймворка способ использования значений из состояния стора в конкретном фреймворке (значение as-is в Реакте, сигнал в Солиде, реф в Vue, etc)
- Правильно реализует под капотом подписку на сторы вместе с батчингом синхронных апдейты от всех предоставленных сторов
- Автоматически обрабатывает ивенты и эффекты и также превращает их в подходящие сущности
Было бы здорово не переизобретать велосипед и переиспользовать useUnit
для биндингов кастомных сущностей из библиотек, т.к. они как рад представляют собой некий шейп юнитов.
Расширить концепцию и реализации useUnit
возможностью задать кастомный способ получения шейпа юнитов с помощью специального символа.
Route из atomic-router представляет собой объект с набором свойств-юнитов:
const postRoute = createRoute()
// postRoute.$isOpened
// postRoute.$params
// postRoute.$query
// ...
Для использования этого postRoute
(например, в Реакте) будет удобно отсечь от свойств сторов их префикс $
, чтобы показать, что речь уже именно о реактивных значениях/сигналах/рефах/etc, но не о исходных сторах.
Реализация может выглядеть так:
- Из
effector
импортируется специальный символ.
import { unitSymbol } from "effector"
- В объект Route добавляется поле с этим символом, в котором находится функция-маппер в любой валидный аргумент для
useUnit
- например, специальный публичный шейп юнитов
{
$isOpened,
$params,
$query,
...,
[unitSymbol]: () => {
return {
isOpened: $isOpened,
params: $params,
query: $query,
}
}
}
Почему функция?
Функция используется для того, чтобы дать лишнее пространство для маневра для пограничных случаев типа effector-factorio
.
Количество вызовов функций должно зависеть от конкретного фреймворка - сколько раз вызван useUnit
в конкретной реализации биндингов, столько раз будет вызвана и функция [unitSymbol]
: В Реакте на каждый рендер, в Солиде на каждый маунт и так далее.
Если кастомная сущность - фреймворк-агностик - то достаточно всегда возвращать один и тот же шейп стабильный шейп. Если библиотека - не фреймворк-агностик - то такое апи оставляет достаточно пространства, чтобы, к примеру, вызвать внутри этой функции хук.
Важно, что определение того, в контексте какого фреймворка происходит работа, должна решаться в рамках самой библиотеки.
- В
useUnit
, если нашелсяunitSymbol
, используется именно этот шейп вместо прокинутого:
const postRoute = createRoute()
// postRoute.$isOpened
// postRoute.$params
// postRoute.$query
const post = useUnit(postRoute)
// post.isOpened
// post.params
// post.query
Это позволит тому же atomic-router
автомагически получить поддержку биндингов сразу ко всем официально поддерживаемым эффектором UI-фреймворкам.
Любые задачи кроме автоматической привязки к View-слою на конкретном UI-фреймворке не могут быть качественно покрыты подобным протоколом.
Кейсы возможных интеграций библиотек между собой слишком узкие, а протокол для useUnit
слишком широк.
Например, возможная интеграция atomic-router
и @farfetched/core
через chainRoute.beforeOpen
подразумевает не произвольный шейп юинтов, а вполне конкретный - Query
из farfetched.
В протокол такое не влезет, но авторы библиотек сами могут договориться между собой о кастомном протоколе взаимодействия.
effector-factorio
на этот момент - библиотека строго под Реакт. Его апи требует явного получения инстанса модели через Реакт-контекст:
const Form = modelView(factory, () => {
return (
<div className="flex flex-col gap-2">
<LoginField />
</div>
})
const LoginField = () => {
const model = factory.useModel()
const login = useStore(model.$login)
return <input
value={login}
placeholder="Login"
onChange={evt => model.loginChanged(evt.target.value)}
/>
}
Предолагаемое использование useUnit
с протоколом может выглядеть так:
const {
login,
...
} = useUnit(factory)
В целом, форма протокола unitSymbol
оставляет достаточно пространства для маневра в подобных случаях:
[unitSymbol]: () => {
const model = factory.useModel()
return factory.mapModelToView(model)
}
но при попытке прокинуть factory
в useUnit
из Solid.js случится ожидаемое исключение - контекст Реакта не взаимозаменяем с аналогичными штуками в Солиде и Vue. По этой же причине не получится абстрагировать все возможные подобные кейсы в рамках unitSymbol
.
Подобные адаптации должны остаться в области ответственности авторов библиотеки.
В случае factorio возможен подобный вариант:
import { modelFactory } from 'effector-factorio';
import { modelView as viewSolid } from 'effector-factorio/solid'
import { modelView as viewReact } from 'effector-factorio/react'
const factory = modelFactory(...)
const Solid = viewSolid(factory, ...)
const React = viewReact(factory, ...)
Лучшим выбором, вместо настоящего символа, будет "магическая" строка вида "@@effectorUnitSymbol"
. Это будет более надежный выбор со множества сторон и в первую очередь - позволит мягкую миграцию библиотекам-пользователям этого апи.
Так можно будет обойтись без импорта "символа" со стороны основного пакета effector
и прост скопипастить строку и всё заработает, как только версия effector-react
будет обновлена до подходящей.
Также API на специальном поле будет проще поддержать во всяких легаси-браузерах.
Разработка, публикация и поддержка отдельных пакетов биндингов для каждой библиотеки под каждый UI-фреймворк.
Для базового юзкейса биндингов - реактивной подписки на поля-сторы и методы-ивенты кастомных сущностей из библиотек - получится обойтись без разработки, публикации и поддержки отдельных пакетов, т.к. можно будет переиспользовать useUnit
из биндингов самого effector
, а это суть есть мгновенная и почти бесплатная поддержка сразу нескольких фреймворков.
Для специальных случаев - интеграции с особенными API фреймворков, в духе solid.createResource
- всё ещё понадобится отдельный пакет.