Немного обсуждений в @effectorjs чате натолкнули меня на мысль изменить подход к описанию логики в effector+react.

Я стараюсь разделять представление и логику. Если взять в пример страницы, рядом с файлом компонента страницы лежал файл логики на эффекторе.
pages/counter/{index.tsx, model.ts}

В файле компонента я напрямую импортировал сторы и ивенты из модели и использовал в компонентах:

import * as React from ‘react’
import { useStore } from ‘effector-react’
import { $counter, incrementClicked, pageMounted } from ‘./model’

export const CounterPage = () => {
const counter = useStore($counter)
React.useEffect(() => { pageMounted() }, [])

// show counter and use events
}


И это работало весьма хорошо, тестировать просто (импортируем сторы в тест и setState(data)). Объявляем контрактом любые экспорты из model.ts и очень осторожно их изменяем.

View-слой (компонент) может независимо меняться от бизнес-логики, и логика может переписываться, если не меняется контракт(экспортируемые сущности).

Но если нужно маппить данные из стора по какому-то ключу или ещё какие операции со списками, то эта логика затаскивалась прям в компонент вместе с useStoreMap и useList. Это мне не очень нравится, ибо тестировать становится сложнее.

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

Использовать хуки, для получения данных и ивентов из модели.
Я предлагаю из модели экспортировать реакт-хуки, вместо сущностей и ивентов. Этот подход имеет смысл в первую очередь для моделей страниц. Разделяемые сущности чаще всего не нужно отдельно заворачивать в хуки, так как часто хочется создавать computed от общих сторов.

Как это выглядит и что это дает: модель экспортирует хуки, которые возвращают наборы данных и ивенты. В этих хуках могут быть любые необходимые вычисления.

import { useCounter, useEvents } from ‘./model’

export const CounterPage = () => {
const counter = useCounter()
const { pageMounted, incrementClicked } = useEvents()

React.useEffect(() => { pageMounted() }, [])

// show counter and use events
}

Можно пойти дальше, и унести React.useEffect() внутрь useEvents, чтобы модель сама устанавливала нужные реакции. Но тут спорно и нужно обдумать. Возможно это нужно выделить в отдельный хук.

А теперь зачем это.
1. Семантика. Теперь, контракт выглядит как независимая сущность. Можем заменять STM как нам захочется, внутри хука может быть React.useReducer, или же Redux.useSelector, или же Effector.useStore. Все равно.
2. Сокрытие сложности. Всякие useStoreMap могут быть скрыты внутри хука, разработчику компонента теперь не нужно знать детали реализации модели, чтобы разработать компонент. Ведь теперь, чтобы рабоать независимо достаточно спроектировать контракт на хуках, описать типы и вернуть dummy-данные.
3. Тестирование. Тесты упрощаются, до мока конкретных хуков, а не сторов. Ведь теперь разработчик компонента, может спокойно написать тесты только на вью, не трогая при этом сторы модели. Логика моделей при этом может быть разработана действительно независимо.

Я пока не могу понять, имеет ли смысл разделять контракт и модель? pages/counter/{contract.ts, model.ts, index.tsx}
- contract — импорты из model завернутые в react-хуки
- model — чистая логика, без примесей хуков
- index — компонент, использующий контракт, без примесей STM

Как думаете?

13 comments

Show 1 more comment
Как круто! Я сейчас в проекте описываю по очень схожему подходу, только к сожалению на редаксе. Рядом с файлом компонента файл model, и там лежит вся локальная логика компонента и походы в редакс. Далее в компоненте я делаю что-то по типу `useModel` и получаю все что мне нужно, но вообще по хорошему это нужно более лучше декомпозировать. Идея с контрактом классная, кажется что было бы удобнее держать хуки которые используют модель отдельно от модели
Yan👀 Lobaty
а если одна модель используется в нескольких вьюхах? и ее ивенты тоже
Ага или к примеру модель фичи. Ее эвенты и сторы часто юзаются не только во вьюхах. Получается надо будет экспортить и хук и сами модели? Или это только для страниц предлагается? Тогда тоже не очень, так как нету единообразия.
Yan👀 Lobaty
а если одна модель используется в нескольких вьюхах? и ее ивенты тоже
Кажется что в этом случае имеет смысл такое выносить в shared/<model>/{contact.ts, model.ts}
Yan👀 Lobaty
а если одна модель используется в нескольких вьюхах? и ее ивенты тоже
Думаю, эта идея будет работать чисто для страниц. Для shared моделей, уже будет по другому. Думаю можно описывать contract рядом с компонентом и подключать в него все необходимые модели
feature/example/components/name/{index.tsx, contract.ts}
Сова
feature/example/components/name/{index.tsx, contract.ts}
а где вы описываете типы модели, прям в самом файле или у вас есть какие нибудь `entities` которая модель юзает ?
Сова
Думаю, эта идея будет работать чисто для страниц. Для shared моделей, уже будет по другому. Думаю можно описывать contract рядом с компонентом и подключать в него все необходимые модели
Не совсем ясно зачем вводить разные сущности для использования одного и того же, будь то в других моделях или вьюхах.
Зачем уходить от единого подхода, который возможен и удобен.
Считаю, что однозначно не стоит заниматься подобным

С таким подходом мы пляшем в другую сторону и протаскиваем в модель (логику предметной области, а именно только это я могу назвать полноценной логикой и бл вообще) данные о лайфсёклах, реакте и бог знает чём ещё
Немного больше обсуждений ещё тут https://t.me/effector_ru/74757