В этой серии мы рассмотрим, как можно сохранить декларативность кода при адаптации функций к все более высоким уровням сложности.
Уровень 7: Селекторы с несколькими магазинами
Теперь представьте, что нам нужно отключить кнопку затемнения, если все цвета уже черные. Теперь нам нужно состояние всех магазинов.
Если бы мы использовали библиотеку управления состоянием, например NgRx, это было бы просто. Мы бы просто создали еще один селектор, подобный этому:
const selectAllBlack = createSelector(
favoriteColors.selectAllAreBlack,
dislikedColors.selectAllAreBlack,
neutralColors.selectAllAreBlack,
(favoriteAllAreBlack, dislikedAllAreBlack, neutralAllAreBlack) =>
favoriteAllAreBlack && dislikedAllAreBlack && neutralAllAreBlack,
);
Но в нашем случае селекторы были определены в адаптере, оторванном от конкретного состояния. Когда мы создаем наши маленькие хранилища, они привязываются к реальному состоянию. Поэтому нам нужен способ извлечь эти селекторы из наших хранилищ и создать с их помощью новые селекторы. И было бы неплохо, если бы они снова были обернуты в наблюдаемые. Таким образом, нам как будто нужен еще один магазин, который объединяет оригинальные магазины:
colorsStore = joinStores({
favorite: this.favoriteStore,
disliked: this.dislikedStore,
neutral: this.neutralStore,
})({
allAreBlack: s =>
s.favoriteAllAreBlack && s.dislikedAllAreBlack && s.neutralAllAreBlack,
})();
Наш новый селектор имеет доступ к селекторам каждого магазина, каждый из которых имеет префикс с ключом объекта, который мы использовали в первом объекте при передаче его магазина. Что это за s
? Я решил сократить до s
, потому что этот объект представляет и производное состояние, и имена селекторов, поскольку они одинаковы. И потому что s
— это коротко, а я люблю печатать меньше 🤷. При наличии только 1 селектора мы используем больше строк кода, чем createSelector
, но когда у нас 2+ селектора, этот подход требует гораздо меньше кода.
Внутри мы можем использовать прокси, чтобы видеть, к каким селекторам обращаются, и строить селекторы ввода динамически. Если первый селектор allAreBlack
никогда не возвращает true
, нам даже не нужно проверять остальные. Оптимизация возможна только потому, что мы можем предположить, что селектор является чистой функцией.
В шаблоне мы можем использовать его следующим образом:
<button
class="black"
(click)="blackout$.next()"
[disabled]="colorsStore.allAreBlack$ | async"
>Blackout</button>
Теперь после нажатия кнопка становится неактивной:
И когда вы меняете один из цветов, кнопка снова становится включенной:
StackBlitz
Наш селектор зависит от селекторов внутри магазинов, но в конечном итоге они были определены в адаптерах. Адаптеры легко тестировать, потому что они не зависят ни от Angular, ни от магазинов, ни от чего, кроме утилит и, возможно, других адаптеров. Логика внутри них полностью независима от конкретных состояний или хранилищ. Разве не было бы здорово, если бы мы могли определить наш новый селектор в собственном адаптере и просто ссылаться на него в вызове функции joinStores
?
Мы могли бы иметь функцию joinAdapters
с синтаксисом, аналогичным синтаксису joinStores
:
const colorsAdapter = joinAdapters<AllColorsState>({
favorite: colorAdapter,
disliked: colorAdapter,
neutral: colorAdapter,
})({
allAreBlack: s =>
s.favoriteAllAreBlack && s.dislikedAllAreBlack && s.neutralAllAreBlack,
})();
// ...
colorsStore = joinStores({
favorite: this.favoriteStore,
disliked: this.dislikedStore,
neutral: this.neutralStore,
})(colorsAdapter.selectors)();
Знаете, что еще хорошего в этом шаблоне? Если по какой-то причине мы решили иметь один магазин вместо трех отдельных, то теперь мы можем использовать этот объединенный адаптер самостоятельно:
colorsStore = createStore(['colors', initialState, colorsAdapter], {
setFavorite: this.favorite$,
setDisliked: this.disliked$,
setNeutral: this.neutral$,
setAllToBlack: this.blackout$,
});
Откуда взялось это новое изменение состояния setAllToBlack
? Не от какого-то отдельного адаптера. Раньше у нас был один источник, который подключался к 3 отдельным реакциям состояния setAllToBlack
, по одной для каждого магазина. Аналогично, когда мы объединяем адаптеры, у нас появляется способ указать эффективные изменения состояния, в которых участвуют несколько адаптеров:
const colorsAdapter = joinAdapters<AllColorsState>({
favorite: colorAdapter,
disliked: colorAdapter,
neutral: colorAdapter,
})({
setAllToBlack: {
favorite: colorAdapter.setAllToBlack,
disliked: colorAdapter.setAllToBlack,
neutral: colorAdapter.setAllToBlack,
},
})({
allAreBlack: s => s.favoriteAllAreBlack && s.dislikedAllAreBlack && s.neutralAllAreBlack,
})();
Это тот же объем кода, что и в случае, когда магазины были раздельными. К сожалению, синтаксис должен быть другим. Теперь вместо того, чтобы кнопка вызывала blackout$.next()
, она будет вызывать colorsStore.setAllToBlack()
, и вместо трех отдельных магазинов, реагирующих на этот источник, у нас есть одна реакция состояния, определяющая три внутренних реакции состояния. Таким образом, синтаксис как бы вывернут наизнанку по сравнению с 3 отдельными магазинами.
Какой способ лучше? Отдельные магазины или объединенный магазин?
Я пока не знаю. Поэтому мне показалось важным, чтобы синтаксис был как можно более схожим, чтобы, если возникнет ситуация, когда один вариант станет предпочтительнее другого, его можно было бы легко изменить.
Целью этой серии было изучить, как избежать синтаксических тупиков на протяжении всего процесса повышения реактивности. Есть ли синтаксис лучше, чем этот?
Я думаю, что это прекрасно, но я бы хотел услышать ваши мысли. Я все еще могу что-то изменить. StateAdapt 1.0 еще не выпущен. Эта серия — способ для меня укрепить синтаксис, чтобы подготовиться к релизу.
Я пытаюсь разработать идеальный синтаксис для 100% декларативного управления состояниями. Однако я также понимаю, что нам нужно знать, как работать с императивными API. Об этом будет следующая статья в этой серии. После этого мы сделаем шаг назад и посмотрим, как добиться максимально декларативного управления состоянием, учитывая текущую экосистему библиотек в Angular и тот факт, что мой любимый синтаксис (StateAdapt) еще не готов к производству.