Эффектор предоставляет хорошие базовые примитивы для построения логики, поверх них можно строить другие абстракции.
Многие библиотеки в экосистеме вводят свои абстракции: Query из farfetched, Route из atomic-router и Factory из factorio, которые обычно представляют собой объект со сторами
Было бы здорово ввести некий стандарт коммуникаций, который бы, вместо кастомных биндингов на каждый случай, позволил делать так:
import { useUnit } from "effector-react"
import { createRoute } from "atomic-router"
const postsRoute = createRoute()
// ...
const { query, params } = useUnit(postsRoute)
Т.е. коммуницировать между собой различным эффектор-библиотекам.
Поскольку самыми базовыми примитивами являются сторы и ивенты, то и сводится всё тоже должно к ним.
Достичь этого можно было бы вводом специального универсального символа контракта, подобно тому, как работает Symbol.observable
:
import { unitSymbol } from "effector"
const $unit = SomeAbstractUnit[unitSymbol]()
- Сам контракт должен быть некой функцией, которая возвращает юнит.
- Эту функцию могут вызывать сколько угодно раз и откуда угодно - поэтому функция не может быть фабрикой юнитов и, в идеале, должна только читать что-то из внешнего скоупа.
- Аргументов у функции, в общем случае, быть не должно.
В специальных случаях возможны исключения, об этом ниже
Сейчас под все основные, поддерживаемые в орге эффектора, UI-фреймворки (React, Vue, Solid.js) уже есть хук useUnit
Было бы здорово переиспользовать его же и для более абстрактных юнитов библиотек.
На примере atomic-router
Имеем:
const postRoute = createRoute()
// postRoute.$isOpened
// postRoute.$params
// postRoute.$query
Контракт должен возвращать юнит, поэтому внутри дополнитльно создается подходящий стор:
const $routeState = combine({ isOpened: $isOpened, params: $params, query: $query })
Реализация контракта:
{
[unitSymbol]: () => {
return $routeState
}
}
Теперь Route умеет сконвертировать себя в базовый юнит и отдать его наружу:
const { query, params, isOpened } = useUnit(postsRoute)
Это будет работать с любым UI-слоем, биндинги которого умеют подписываться на стор
На примере @effector/reflect + effector-factorio
Рефлект ожидает юниты в биндах для пропсов, эффектор-факторио генерирует пару фабрика-вьюха, обе библиотеки для реакта.
Их взаимодействие может выглядеть подобным образом:
import { modelView, modelFactory, selectUnit } from "effector-factorio"
import { reflect } from "@effector/reflect"
const factory = modelFactory(() => {
// код фабрики
return {
$login,
loginChanged,
};
});
const Form = modelView(factory, () => {
return (
<div className="flex flex-col gap-2">
<LoginField />
</div>
})
const LoginField = reflect({
view: Input,
bind: {
value: selectUnit(factory, "$login"),
onChange: selectUnit(factory, "loginChanged"),
disabled: $globalBlock,
}
})
Хелпер selectUnit
мог бы производить объект с контрактом такого вида:
const selectUnit = (factory, selectedUnit) => ({
[unitSymbol]: () => {
// факторио извлекает конкретный инстанс модели в рантайме реакта из контекста
const model = factory.useModel()
return model[selectedUnit]
}
})
Рефлект мог бы вычислять контракты во время рендера компонента и таким образом получать корректный юнит в рантайме.
Таким образом, в этом случае контракт по сути будет кастомным хуком - обе библиотеки для Реакта, поэтому такое сведение допустимо.
В случае если обобщить их на разные фреймворки, то подобный selectUnit
может быть у каждой версии свой и поддерживать специфичные для UI-фреймворка штуки.
Отсюда же вытекает дополнительное ограничение, что если контракт предназначен для склеивания разных библиотек между собой в контексте общего фреймворка, то:
- Контракт должен производиться отдельным хелпером
- Контракт должен вызываться в контексте фреймворка
В первом кейсе видно, что проебана важная часть - и Query, и Route, и наверняка другие юниты из библиотек будущего имеют помимо сторов и ивенты-методы, которые нужно вызывать в юзер-ленде.
Этим библиотекам, возможно, пригодилось бы подобное:
const route = useUnit(postRoute)
route.isOpened // читаем состояние
route.open() // вызываем метод
// всё в скоупе, по уму
Но, очевидно, свести в один юнит и сторы, и ивенты просто так не получится, т.к. ивенты, например, нужно будет прибить к текущему скоупу из Provider, если мы находимся в контексте Реакта. Ивент таким образом в комбайн не подсунешь.
Возможный выход проблемы выше - разрешить возвращать не только один юнит,но и шейп:
{
[unitSymbol]: () => {
return { isOpened: $isOpened, params: $params, query: $query, open }
}
}
Но это создает новую проблему - теперь есть неоднозначность поведения во многих случаях, например, вот в таком:
С контрактом, который всегда возвращает юнит
const {
value,
route: { isOpened }
} = useUnit({
value: $value,
route: postRoute // резолвится однозначно 👍
})
С контрактом, который возвращает либо юнит, либо шейп
const {
value,
route: { isOpened }
} = useUnit({
value: $value,
route: postRoute // ❓❓❓
})
В случае useUnit
появляется необходимость, по сути вложенного вызова этого же хука в некоторых случаях.
Для взаимодействия не-базовых библиотек между собой это создает ещё больше проблем и неоднозначностей
reflect({
view: Page,
bind: {
value: // в рефлекте идеологически нельзя привязать шейп разнородных юнитов к одному пропу
}
})
Сейчас и atomic-router и farfetched не дают встроенных хуков-инструментов для вызова методов, в целом подразумевается, что вьюха этого не делает напрямую - только через ивенты в духе buttonClicked
, описание этого должно быть в модели, а не вью.
Поэтому, как-будто бы, ни шейпы, ни методы пока не нужны, а одного стора или ивента в качестве универсального стандарта достаточно
Но, возможно есть другие кейсы, которых я не вспомнил.
Мои аргументы против шейпа
Стор/ивент - универсальные примитивы, а вот шейп - уже нет, об этом и проблема, дело не только в типах.
Потому что произвольный шейп - не юнит и его нельзя просунуть везде, где можно просунуть юнит, вызовы
useUnit(arg | shape)
- исключениеРефлект - пример либы, где шейп юнитов в апи невпихуем совсем и никак.
С другой стороны, возможно, рефлект+факторио это какой-то специальный случай и ради этих странных чуваков ограничивать всех не стоит
Случай комбинации атомика и фарфетчед
Этот пример редставляется мне странным.
Вот прилетело в
chainRoute.beforeOpen
нечто, что определило себе контракт юнита-шейпаКак чейн-роут поймет, что ему с этим делать?
Если это нечто должно предоставить не просто шейп, а именно ивент start и стор $status - то это уже другой контракт, по сути, "chainRoute.beforeOpen принимает Query"
Это уже не универсальный контракт, а вполне конкретная интеграция между двумя конкретными библиотеками, которая не будет работать ни для кого другого