Структура моделей Effector
Table of contents
Введение
Во время работы, у меня есть три состояния:
- Читаю существующий код
- Изменяю существующий код
- Пишу новый код
Во время чтения и изменения кода, мне необходимо четко понимать какую задачу решает каждый кусок кода в файле, в какой сценарий складываются эти кусочки и как работают вместе файлы логики в проекте.
Написание нового кода, я начинаю с проектирования внешнего интерфейса файла, с событий которые необходимых в этом блоке логики.
Какие-то файлы описывают user story, другие — сущности. Детали подхода для каждого из типов файлов несколько отличаются, но код однозначно должен читаться сверху вниз: определения, логика, детали реализации.
Model
Вне зависимости от типа, я называю модуль с логикой на эффекторе — модель.
User Story
Чаще всего один или несколько схожих сценариев, решающих одну задачу пользователя. Такой тип модели использую в страницах и организмах фичи, подробности в предыдущей статье Структура приложения.
Entity
Данные описывающие одну сущность и события ее модифицирующие. У таких моделей обычно нет сценария использования и они лежат в models/
директории фичи.
Структура
- Определения
- Логика
Модуль поделен на логические блоки идущие в строгом порядке, блоки разделены между собой пустой строкой.
Порядок блоков в модели:
- Типы
- События
- Эффекты
- Сторы и вычисляемые сторы
- Логика в виде связей
- Детали реализации
Типы
- В начале определяю типы используемые в модуле. Именно в начале, чтобы любой разработчик сразу мог определить словарь имен типов.
- Так как импорты должны идти самыми первыми в файле, удобно видеть сразу и типы созданные в текущем модуле и заимпорченные.
- Размещаю здесь необходимые константы
type InputEvent = SyntheticEvent<HTMLInputElement>;
type Form = {
login: string;
password: string;
remember: boolean;
};
const MINIMUM_TIMEOUT = 100;
События
В этом блоке описываю приватные и публичные события. Когда усаживаюсь проектировать, начинаю именно с этого блока, проставляя всем событиям тип
*
. После определения интерфейса модуля указываю конкретные типы.Все события именую в camelCase, в прошедшем времени.
Формат именования:
[неймспейс] сущность событие
Событие описывает “что произошло”.
Имена событиям пропишет Babel plugin effector.
export const pageMounted = createEvent<void>();
export const loginChanged = createEvent<InputEvent>();
export const buttonPressed = createEvent<ButtonEvent>();
const buttonTypePressed = submitPressed.map(
(event) => event.currentTarget.type,
);
const loginSaveFailed = createEvent<string>();
Эффекты
- Только декларация эффекта, без привязки к handler.
- Flow требует указания дженерик-типов, если эффект экспортируется.
- Имена эффектов также как и событий пропишет Babel plugin effector.
- Эффекты именую глаголом с существительным.
- При необходимости под каждым эффектом описываю его fetching.
export const saveLogin = createEffect();
const saveLoginFetching = createFetching(saveLogin, 'loading');
export const loadLogin = createEffect();
Сторы и вычисляемые сторы
- Сторы именую существительным с префиксом
$
, чтобы всегда однозначно знать где стор. Это нужно, так как почти везде где типизация пустит событие, подойдет и стор. Необходимо иметь возможность с первого взгляда распознать стор не ожидая подсказки типов в IDE. При вводе$
IDE подскажет какие сторы имеются, чтобы быстро заимпортить. - Boolean сторы именую с префиксами
is
,has
,was
, … - Начальное значение передаю в аргументы
createStore
, так не нужно создавать лишнее имяstoreNameInitial
. - Вычисляемые сторы ниже обычных и отделены пустой строкой.
const $login = createStore('');
const $password = createStore('');
export const $isLoginValid = $login.map(loginValidator);
export const $isPasswordValid = $login.map(passwordValidator);
export const $isFormValid = eachTrue([
$isLoginValid,
$isPasswordValid,
]);
export const $form = combine({
login: $login,
password: $password,
});
Логика в виде связей
- Здесь только связи между определениями и импортами
- Я группирую и сортирую блоки кода так, чтобы логика модели читалась сверху вниз
$login.on(loginChanged.map(trimEvent), (_, login) => login);
$login.reset(pageMounted);
sample($form, submitPressed).watch(loginUser);
loginUser.use(authApi.login);
Детали реализации
- В этом месте определяются все вспомогательные функции, для этого они должны быть описаны как function declaration
function trimEvent(event) {
return event.currentTarget.value;
}