На Хабре вышла хорошая статья «Нужен ли здесь useEffect?»: 12 сценариев из code review, в которых эффект либо не нужен, либо нужен не такой.Я пишу на $mol и читал её как этнограф. в моле нет API эффектов. Во что превратятся все 12 демо, если их переписать ? Посмотрите ?. Сделал страницу с демками.Спойлер: чек-лист из 12 строк в конце схлопнулся в … Узнать про useEffect в $mol
Здравствуйте, товарищи! На Хабре вышла хорошая статья «Нужен ли здесь useEffect?» Виктора Горбачёва ( @Viktoriko ) : 12 сценариев из code review, в которых эффект либо не нужен, либо нужен не такой. Разбор я особо конечно не могу понять, так как пишу только на…
$mol’e и читал статью как этнограф. Каждое демо начинается с вопроса «а эффект здесь вообще нужен?», а мне этот вопрос не задать ведь в моле нет API эффектов. Стало интересно переписать. Переписал. Получилась страница с демками и этот текст.
У кода в React три места выполнения. Рендер для чистых вычислений, обработчик события для действий пользователя, эффект для синхронизации с внешней системой. И два вопроса перед каждым useEffect: можно ли это значение вычислить, и почему этот код должен выполниться.
В моле мест два. Вычисление, то есть обычный метод компонента: если результат стоит кэшировать, вешаешь на него @$mol_mem, и значение пересчитывается только когда меняется что-то из прочитанного внутри. И действие: декоратор @$mol_action, на который в view.tree вешается клик. Рендер отдельным местом не считается, он сам такое же вычисление. Синхронизация с внешним миром, забегая вперёд, тоже.
Пройдём по всем 12 демо. Нумерация и названия частей как у Виктора
fullName в useState, синхронизируемый эффектом — антипаттерн, правильно вычислять его во время рендера. В моле правильный вариант единственный, производное состояние просто негде хранить:
full_name() {
return `${ this.first_name() } ${ this.last_name() }`
}
Метод, даже без mem. Кто прочитал full_name, тот перерисуется при смене имени. Запомните это совпадение: «хороший» React-вариант и мол-вариант здесь один и тот же код, дальше так будет ещё не раз.
Дорогой расчёт в React мемоизируют через useMemo, перечислив зависимости в массиве. В моле это метод из прошлого демо, только с декоратором:
@ $mol_mem
visible_todos() {
// какой-то измеримо дорогой расчёт
const active = this.filter() === 'active'
return this.todos().filter( t => active ? !t.done : true )
}
Пересчёт идёт только при смене фильтра или списка, в демо это видно по console.log. Дилемма «лишний useState или useMemo» не возникает, mem и есть мемоизация. Массива зависимостей тоже нет: mem запоминает, что читал filter() и todos(), по факту чтения.
Сброс черновика при смене userId в React решается пересозданием поддерева через key. В моле состояние можно хранить сразу по ключу:
@ $mol_mem_key
comment( user: string, next?: string ) {
return next ?? ''
}
@ $mol_mem
current_comment( next?: string ) {
return this.comment( this.user(), next )
}
У каждого пользователя свой черновик. Переключился на Бориса, вернулся к Анне а её недописанный комментарий на месте. В React-версии черновик при возврате не выживал, там это отдельная фича, которую надо проектировать, а тут оно само получается.
Виктор советует хранить selected_Id, а объект вычислять через find, без эффекта. Спорить не о чем, его правильный вариант уже готовый мол-код:
selection() {
return this.items().find( i => i.id === this.selected_id() ) ?? null
}
Удалили выбранный элемент, selection сам стал null, без дополнительного обновления состояния.
Уведомление «Добавлено в корзину» через эффект от product.isInCart срабатывает и тогда, когда корзина восстановилась из хранилища. Общую логику Виктор выносит в обычную функцию, и это ровно то, что пишется в моле:
buy() {
this.cart_count( this.cart_count() + 1 )
this.status( `В корзине товаров: ${ this.cart_count() }` )
}
@ $mol_action
to_cart() {
this.buy()
}
@ $mol_action
buy_now() {
this.buy()
this.status( 'Переходим к оплате…' )
}
Вывод в статье мне нравится: не всякое переиспользование требует хука. В моле хуков нет, так что и соблазна не возникает)
Сигнальное состояние, эффект на него, а в React 19 Actions и useActionState на два десятка строк. В моле запрос пишется прямо в действии, синхронным по виду кодом:
@ $mol_action
submit() {
this.sending( true )
this.$.$mol_wait_timeout( 800 ) // тут был бы запрос
this.count( this.count() + 1 )
this.result( `Создан пользователь #${ this.count() }` )
this.sending( false )
}
Под капотом файбер (fiber): на время ожидания метод приостанавливается, кнопка задизейблена через sending, потом появляется результат. useActionState кстати неплохой API, pending и результат лежат рядом с причиной. Просто в моле это свойство любого действия, а не отдельная фича форм.
Игру с картами, где три эффекта триггерят друг друга, Виктор сворачивает в одну чистую функцию перехода. Мол-версия выглядит так же:
@ $mol_action
place_card( gold: boolean ) {
this.game( this.next_state( this.game(), gold ) )
}
В React такой каскад собрать легко, он даже работает, поэтому и попадает в прод. В моле его нельзя собрать: эффектов нет, а состояние меняется в действиях, одним переходом.
Ребёнок сообщает родителю через onChange, и вопрос лишь в том, звать колбэк из эффекта (плохо) или из обработчика (хорошо). В моле этот код не пишется вовсе, связь описывается в view.tree:
<= Toggle $mol_check_box
checked? <=> light_on? false
<= Status $mol_view
sub / <= light_status
<=> это двусторонний биндинг: тумблер и статус читают одну ячейку light_on. Ни колбэка, ни двух кругов обновления, ни выбора между controlled и uncontrolled. Если код выглядит “страшно” или непонятно, то гляньте сюда view.tree это просто краткая запись для нашего любимого JS
код выше будет представлен так
light_on(next){
if(next !== undefined) return next;
return false;
}
Toggle(){
const obj = new this.$.$mol_check_box();
(obj.checked) = (next) => ((this.light_on(next)));
return obj;
}
Status(){
const obj = new this.$.$mol_view();
(obj.sub) = () => ([(this.light_status())]);
return obj;
}
sub(){
return "\t";
}
Глава про пустой массив зависимостей, Strict Mode с двойным вызовом и guard-флаг в точке входа. В моле её заменяет одно свойство рантайма: всё лениво. Никакой мем не вычисляется, пока его кто-нибудь не прочитает.
@ $mol_mem
visits() {
return Number( this.$.$mol_state_local.value( 'bog_effects_visits' ) ?? 0 )
}
В демо панель со счётчиком не считается, пока её не раскрыли. Вопрос «когда выполнить инициализацию ?» превращается в «кому нить нужны эти данные ?». Если никому то она не выполнится вообще
useSyncExternalStore раскладывает подписку на subscribe, снапшот и серверный снапшот, плюс требование стабилизировать subscribe, иначе переподписка. В моле внешний источник оборачивается в обычное реактивное свойство:
@ $mol_mem
online( next?: boolean ) {
if( next !== undefined ) return next
const win = this.$.$mol_dom_context
win.ononline = ()=> this.online( true )
win.onoffline = ()=> this.online( false )
return win.navigator.onLine
}
Подписка происходит при первом чтении, дальше браузер пушит значение через гетеро-сеттер, читатели перерисовываются. Часы в демо ещё короче: $mol_state_time.now(1000) из коробки, причём таймер там живёт, только пока часы кто-то читает исходник на 25 строк всего.
Моё любимое демо. Соединение с чатом зависит от roomId, но не должно пересоздаваться от смены theme, и чтобы вынести тему из зависимостей эффекта, в React 19.2 появился отдельный API. В моле зависимости не декларируются, они собираются по факту чтения:
@ $mol_mem
connection() {
const room = this.room()
const conn = Object.assign( new $mol_object2, { room } )
conn.destructor = ()=> console.log( 'disconnect', room )
return conn
}
notice() {
return `Комната «${ this.connection().room }», тема: ${ this.theme() }`
}
Тело connection читает только room, значит соединение зависит только от комнаты. Тема читается в notice, её смена перерисовывает уведомление, не трогая соединение. А при смене комнаты старое соединение закрывает деструктор. На демо-странице есть счётчик переподключений, пощёлкайте тему и посмотрите на него.
useEffectEvent это ручное исключение из ручного списка зависимостей. Когда списка нет, исключать не из чего.
Здесь Виктор честно пишет: загрузка по query это настоящая синхронизация с сервером, эффект не концептуальная ошибка, опасна наивная реализация. Гонка ответов, отмена, кэш, ретраи. Дальше четыре подхода на выбор: ручной хук с AbortController, TanStack Query, use() с Suspense, RTK Query.
Мол-вариант один:
@ $mol_mem
search() {
const q = this.query().trim().toLowerCase()
if( !q ) return []
this.$.$mol_wait_timeout( 600 ) // имитация сети запрос через $mol_fetch
return this.products().filter( p => p.toLowerCase().includes( q ) )
}
Пока «сеть» отвечает, вычисление приостановлено, ожидание мол показывает сам. Пользователь напечатал новую букву — устаревшее вычисление отменяется, а с ним и запрос: $mol_fetch в деструкторе дёргает AbortController (исходник). Гонка невозможна, кэш это сам mem. В демо есть счётчик завершённых «запросов»: печатайте хоть по буквам, он почти не двигается.
TanStack Query добавляет инвалидация по мутациям, оптимистичные апдейты, ретраи, devtools. Мол закрывает гонки, отмену и кэш самим рантаймом, а вот эти плюшки TanStack, если понадобятся, собираешь руками.
Статья Виктора заканчивается таблицей на 12 строк: задача, что проверить перед тем как писать эффект, номер демо. Хорошая таблица, я бы повесил такую рядом с доской, если бы писал на React.
Моя версия целиком:
Задача | Что делать |
|---|---|
Вычислить значение | метод, если дорого — с |
Отреагировать на действие пользователя | метод с |
Это упрощение не ради красивого финала, все 12 сценариев действительно можно разложить на эти две строки. Дерево решений не нужно.
У Виктора есть мысль, которая мне нравится: повторяющееся замечание на review надо превращать в правило команды. Это работает в любом стеке. Просто любопытно наблюдать, сколько таких правил исчезает, когда реактивность лежит в основании фреймворка, а не прикручивается к нему сверху. ( беря реакт, вы собираете свой фреймворк, по сути, а надо ли ? )
Все 12 демо живут на странице: b-on-g.github.io/bog/effects, исходники тут. Потыкайте с открытой консолью, самое интересное в ней)
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
0%Ни одного, каждый на своём месте0
0%У меня нет useEffect в проекте)0
Проголосовал 1 пользователь. Воздержавшихся нет.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Мобильная разработка за неделю #636 (22 — 28 июня) | 5 | 7 | 28-06-2026 |
| 2 | Когда может пригодиться экзотика в ООП: миксины/трейты/аспекты | 0 | 5 | 08-07-2026 |
| 3 | std::expected в C++23: гайд по миграции с исключений на функциональный error handling | 0 | 7 | 07-07-2026 |
| 4 | Попросили Claude создать WCAG-доступный DataPicker на React и потратили 3 дня на доработки | 0 | 7 | 28-06-2026 |
| 5 | Актуальность техстека как задача: наш путь от хаоса к регулярным SLA-апдейтам | 5 | 7 | 26-06-2026 |
| 6 | Ищу того, кто хорошо разбирается в hugo (site generator) и может плюс-минус в js/css | 0 | 5 | 24-06-2026 |
| 7 | WP Rocket ухудшает mobile LCP | -2 | 3 | 26-06-2026 |
| 8 | Регулируй регулярки! Выбираем инструменты для работы с RegEx | 5 | 7 | 25-11-2025 |
| 9 | Провожу ещё один эксперимент. Игра "Космическая данетка". Любите читать про ... | 0 | 2 | 30-06-2026 |
| 10 | Рябков: разрабатываемые США ракеты-мишени для систем ПРО нарушают ДРСМД | 0 | 0 | 26-11-2018 |