Стадии объявления и инициализации.
Эффектор-модели создаются в две стадии:
- Стадия объявления. На этой стадии создаются сторы, эвенты и эффекты.
export const catalog = root.domain('catalog')
export const $productsList = catalog.store<Product[]>([])
export const init = catalog.event<void>()
export const reset = catalog.event<void>()
export const getProductsFx = attach({
effect: productsClient.getProductsReqFx,
mapParams: () => ({ limit: PRODUCTS_PAGINATION_LIMIT, offset: 0 }),
})
- Стадия инициализации. На этой стадии связываются сторы, эвенты и эффекты, объявляются сайд-эффекты.
// reset all stores on 'reset' event
catalog.onCreateStore((store) => store.reset(reset))
/* use cases logic */
$productsList.on(getProductsFx.doneData, (currentList, products) => [
...currentList,
...products,
])
// get products on init
forward({
from: init,
to: getProductsFx,
})
Очень важно разделить эти две стадии по различным модулям (файлам).
- В модулях с объявлениями создаются сторы, эвенты и эффекты без какой либо бизнес-логики. Эти модули могут и должны экспортироваться во внешний мир.
- В модулях инициализации содержится только чистая бизнес-логика. Эти модули никогда ничего не экспортируют.
Тут можно задаться логичным вопросом: как же бизнес-логика, содержащаяся в модулях инициализации, вообще попадет в бандл? Через точку входа. Точка входа импортирует модуль init.ts, в котором импортируются init
-модули из всех модулей приложения.
Пробегитесь по этим цепочкам импортов в демо-примере, чтобы лучше понять, о чем идет речь:
src/index.tsx > src/init.ts > src/features/app/init.ts > src/features/app/model/init.ts
src/index.tsx > src/init.ts > src/features/products-list/init.ts > src/features/products-list/model/init.ts
src/index.tsx > src/init.ts > src/features/cart/init.ts > src/features/cart/model/init.ts
Каждая фича предоставляет init
-файл, в котором импортит init
-файлы своих моделей (если их несколько). Это нужно для инкапсуляции содержимого модуля: точка входа не должна знать о том, какие модели содержатся внутри модулей: она просто импортит feature/init
.
Здесь можно задаться вопросом: "зачем разбивать бизнес-логику и объявления по разным модулям? Почему бы не написать все в одном месте?". И на то есть две очень веские причины:
- (субъективная) Разбиение на отдельные модули улучшает читаемость модели. Читая бизнес-логику, вы не отвлекаетесь на рутинные объявления сущностей
- (объективная) Разбиение на отдельные модули поможет избежать падения вашего приложения, если в нем вдруг обнаружатся циклические зависимости.
Если с первым пунктом все ясно, то на втором остановимся подробнее. Да, любое современное окружение (включая webpack+typescript+es6 modules) легко справляется с циклическими зависимостями. Однако любой алгоритм разрешения циклических зависимостей (включая тот, который используется в webpack) допускает ситуацию, при которой В НЕКОТОРЫЙ момент инициализации приложения оказывается невозможно предоставить модулю его зависимость (она будет предоставлена позднее). В случае webpack в этот момент времени эта переменная будет undefined
. Это создает проблемы, так как наши модули инициализации моделей содержат сайд-эффекты и обязательно требуют наличия всех своих зависимостей:
import { someEvent } from '../../anotherModel'
import { $someStore } from './state'
$foo.on(someEvent, (prevState, payload) => ({ ...prevState, ...payload }))
Если на момент того, когда любой модуль нашего приложения импортировал данный модуль, переменная someEvent
окажется undefined
(из-за циклической зависимости), это приведет к исключению в глобальной области, которое почти наверняка уронит все приложение.
Чтобы избежать этой неприятной ситуации, мы отделяем бизнес-логику в отдельный файл, который импортится непосредственно в точке входа, причем ПОСЛЕ всего остального. В этот момент все фичи и страницы уже импортированы, и возможные циклические зависимости разрешены, поэтому мы можем быть уверены, что не наткнемся на undefined
.
Описанная здесь схема в действительности является частью общего правила, применимого не только к эффектор-моделям:
Сайд-эффекты в корне модуля, который экспортирует что-либо, недопустимы. Такой модуль не должен экспортировать.
К слову, эта проблема актуальна даже при использовании сервис-контейнеров. Как правило, любая реализация сервис-контейнера жестко разделяет стадию создания и стадию инициализации, на которой допустимы сайд-эффекты.
P. S. Помните, что любой вызов функции, в которой в качестве аргумента передается переменная, импортированная из другого модуля, — это потенциальный сайд-эффект. Более того, вызов любой функции, которая может выбросить исключение, — это сайд-эффект (речь идет не о багах, а об ожидаемых исключениях).
Этот код содержит сайд-эффекты:
someFunc(a)
JSON.parse(b)
Этот код не содержит сайд-эффекты:
const $s = createStore({ a: 0 })
const client = new HttpClient()
Отделение создания от инициализации является хорошей устоявшейся практикой в эффектор-сообществе: https://effector.now.sh/docs/conventions/best-practices#file-structure