Sergey Sova

Sergey Sova

Frontender, Podcaster, Team-Lead, Rust-lover, Effector Evangelist writes about development.

Структура моделей Effector

August 21, 2019

Во время работы, у меня есть три состояния:

  • Читаю существующий код
  • Изменяю существующий код
  • Пишу новый код

Во время чтения и изменения кода, мне необходимо четко понимать какую задачу решает каждый кусок кода в файле, в какой сценарий складываются эти кусочки и как работают вместе файлы логики в проекте.

Написание нового кода, я начинаю с проектирования внешнего интерфейса файла, с событий которые необходимых в этом блоке логики.

Какие-то файлы описывают user story, другие — сущности. Детали подхода для каждого из типов файлов несколько отличаются, но код однозначно должен читаться сверху вниз: определения, логика, детали реализации.

Model

Вне зависимости от типа, я называю модуль с логикой на эффекторе — модель.

User Story

Чаще всего один или несколько схожих сценариев, решающих одну задачу пользователя. Такой тип модели использую в страницах и организмах фичи, подробности в предыдущей статье Структура приложения.

Entity

Данные описывающие одну сущность и события ее модифицирующие. У таких моделей обычно нет сценария использования и они лежат в models/ директории фичи.

Структура

  • Определения
  • Логика

Модуль поделен на логические блоки идущие в строгом порядке, блоки разделены между собой пустой строкой.

Порядок блоков в модели:

  1. Типы
  2. События
  3. Эффекты
  4. Сторы и вычисляемые сторы
  5. Логика в виде связей
  6. Детали реализации

Типы

  • В начале определяю типы используемые в модуле. Именно в начале, чтобы любой разработчик сразу мог определить словарь имен типов.
  • Так как импорты должны идти самыми первыми в файле, удобно видеть сразу и типы созданные в текущем модуле и заимпорченные.
  • Размещаю здесь необходимые константы
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 = createStoreObject({ login: $login, password: $password })

Логика в виде связей

  • Здесь только связи между определениями и импортами
  • Я группирую и сортирую блоки кода так, чтобы логика модели читалась сверху вниз
$login.on(loginChanged.map(trimEvent), (_, login) => login).reset(pageMounted);

sample($form, submitPressed).watch(loginUser);

loginUser.use(authApi.login);

Детали реализации

  • В этом месте определяются все вспомогательные функции, для этого они должны быть описаны как function declaration
function trimEvent(event) {
  return event.currentTarget.value;
}