«Он слишком занят самой жизнью, чтобы еще задумываться над ней».
— Джек Лондон, «Морской волк»
Я не React разработчик. Я прибыл к вам с далёкой планеты с одной целью: внедриться и изучить слабые места вашей архитектуры, ваших подходов, дабы доказать своему народу, что ваша цивилизация слишком хаотична, чтобы создать что-то по-настоящему долговечное.
Но в процессе внедрения, произошло то, чего никто не мог предугадать. Я полюбил вашу технологию. И теперь я здесь, чтобы дать вам шанс исправиться, указав вам на ваши проблемы.
Моя главная претензия к современному реактивному сообществу проста: вы потеряли фундамент. Вы строите небоскребы на болоте, игнорируя три фундаментальных правила игры, которые заложили авторы библиотеки.
Претензия №1: Реактивность застывшего кадра
Начнем с основ.
Когда мы пишем в чистом JS, то мы привыкаем мыслить императивно: если нажата кнопка X, покрась блок Y в синий. Т.е мы воспринимаем DOM как что-то поддающееся изменениям.
Эта привычка, в дальнейшем, перетекает и в разработку на React. Многие воспринимают его как пластилин (привет, Vue или MobX), из которого можно лепить живые данные, магически общающиеся друг с другом в реальном времени. Но это не так. Компонент в реакте — всего лишь застывшее мгновение.
Представьте, что вы зашли на сайт и нажали Ctrl + S. Вы скачали статичную копию страницы в текущий момент времени. Да, в этой копии могут быть зашиты скрипты и кнопки, на которые можно нажать, но сам по себе этот файл — зафиксированный снимок прошлого.
Когда вы вызываете компонент, React делает это:
Берет состояние на данную наносекунду.
Прогоняет его через вашу функцию.
Выплевывает описание интерфейса, где все переменные и функции "заперты" внутри этого конкретного кадра.
И проблемы здесь начинаются тогда, когда разработчик пытается оживить кадр из прошлого, вместо того, чтобы подготовить данные для будущего. Так и образуется тягучий кисель из побочных эффектов, где вы идете против физик�� библиотеки.
Пример (плохо):
export const AudioPlayer = () => { const [volume, setVolume] = useState(0.5); const [volumePercent, setVolumePercent] = useState(defaultVolumePercent); const [queue, setQueue] = useState([]); const [isShuffled, setIsShuffled] = useState(false); const [isPlaying, setIsPlaying] = useState(false); // Проблема: Синхронизация стейта через эффект (лишний рендер) useEffect(() => { const value = Math.round(volume * 100); setVolumePercent(value); }, [volume]); } // Проблема: Реакция на изменение флага вместо действия useEffect(() => { if (isPlaying) { if (!isShuffled) { setQueue([]); } else { setQueue((prev) => { return [currentTrackIndex, ...prev]; }); } } }, [isShuffled]); //...
Что мы тут видим?
1) Когда обновилась volume - обнови volumePercent
2) Когда обновился флаг isShuffled - обнови queue
По другому говоря: когда случится А, сделай Б. Звучит логично? Для императивного подхода — безусловно.
«Да-да, я слышал про императивный подход. А при чем здесь Римская империя?»
На этот случай поясню: императивный подход — это управление процессом через прямое изменение состояния; когда твой код является инструкцией по шагам.
Каждый из нас в душе Цезарь. Мы любим раздавать приказы своим слугам: «Ступай в сад, принеси мне красный виноград. Помой его, перетопчи, сок залей в дубовые бочки, дай настояться год и только тогда принеси мне бокал вина на серебряном блюдце!». Вы описываете каждый вздох, контролируя процесс превращения горсти ягод в напиток.
Но React не про это. Его декларативная природа не хочет знать про «процесс». Она хочет знать только цель. У вас есть кадр №1 — «Вина нет» и кадр №2 — «Вино есть».
Но проблема «плохого» примера с useEffect не в том, что он идеологически «не декларативный». Проблема в том, что вы используете его как костыль для имитации императивной цепочки.
Давайте избавимся в текущем примере от подхода «когда случится А, сделай Б»
Пример (хорошо):
export const AudioPlayer = () => { const [volume, setVolume] = useState(0.5); const [queue, setQueue] = useState([]); const [isShuffled, setIsShuffled] = useState(false); // Избавились от лишнего useState и ререндера // Вместо этого стали вычислять значение на лету const volumePercent = Math.round(volume * 100); // Логика шаффла вынесена в действие // Меняем всё за один раз, готовя данные для след. кадра const handleToggleShuffle = () => { const nextShuffleState = !isShuffled; setIsShuffled(nextShuffleState); if (nextShuffleState) { setQueue(prev => [currentTrackIndex, ...prev]); } else { setQueue([]); } }; //...
Итог преобразований:
1) Теперь volumePrecent превратилось в вычисляемое состояние. Больше никаких эффектов или лишних рендеров. Теперь это значение существует только как проекция volume
2) Логика из эффекта вынесена в действие. Мы императивно меняем стейт, но теперь мы делаем это в момент действия, а не в момент изменения флага.
В первом примере:
trigger → render (new isShuffled, old queue) → commit → paint → useEffect → setQueue → trigger → render → commit
Во втором:
click → handleToggleShuffle → setIsShuffled + setQueue → trigger → render → commit
И тут мы плавно перетекаем во вторую претензию.
Претензия 2: Дивный грязный мир
Вторая претензия вытекает из первой: вы используете инструмент синхронизации как инструмент управления.
В идеальном мире React никакой «грязной» логики внутри компонентов быть не должно. Под «грязью» я подразумеваю побочные эффекты: когда функция не просто возвращает UI на основе данных, а начинает общаться с внешним миром — стучаться в какой-нибудь Web API, дергать DOM-дерево или подписываться на глобальные события. Но мы живем в суровой реальности, где без этого никуда.
Для этого и придумали useEffect. Это то, что позволяет обмениваться вашему чистому снимку c тем самым грязным миром (фу-фу-фу). Но использование useEffect для связи двух useState внутри одного компонента — это уже какое-то извращение.
Знаете, мне кажется это даже немного забавным. Видимо, у вашего сообщества с этим хуком есть какие-то свои старые счеты, раз вы используете его насколько не по назначению.
Масштаб этой беды лучше всего виден по официальной документации: в ней содержится несколько огромных статей-инструкции посвященных использованию useEffect. Когда создатели инструмента вынуждены писать многостраничные методички о том, как НЕ использовать их собственную фичу — это ярчайший маркер того, насколько рядовой разработчик забрел не туда.
Приведу аналогию, представьте: международная курьерская служба используется для того, чтобы передать записку человеку за соседним столом.
«Какого дьявола?» — спрашивает у вас коллега, сидящий в метре, когда к нему подходит курьер в желтой куртке и с серьезным видом протягивает конверт с надписью «Привет, как дела?».
А теперь представьте, что таких «порталов» у вас несколько:
Вы пишете записку и вызываете курьера (меняете стейт A).
Сосед получает её, пишет ответ и... вызывает другого курьера (эффект на A меняет стейт B).
Второй курьер везет ответ вам (эффект на B дергает логику).
Это не надежно, сложно поддерживать, и дебаг такого кода превращается в дешевый детектив.
Количество матов в минуту (М/мин) при чтении растет экспоненциально. Я предлагаю ввести это как официальную метрику оценки кода и/или архитектуры. Если ваш код требует 10 М/мин только для того, чтобы понять, куда улетел клик, то вам стоит задуматься над тем, как это можно исправить.
Резюмируя: useEffect — для синхронизации с внешним миром. С тем, что не касается хуков и методов самого React. Всё остальное, скорее всего — это попытка вылечить симптомы вместо болезни.
Претензия 3: Главный принцип, или когда бухгалтер чинит станок
Третья и самая масштабная претензия — это то, как вы размываете ответственность.
Взгляните на API библиотеки. В Реакте всего около 18 хуков (на данный момент) и чуть больше двух десятков APIs Reference методов. Это сознательный, почти радикальный минимализм. Авторы как бы говорят нам: «Смотрите, мы — всего лишь тонкая прослойка между вашими данными и пикселями. Мы не фреймворк для управления логикой адронного коллайдера. Мы просто рисуем то, что вы нам дадите».
Я уже слышу ваши возражения. «Но постойте!» — воскликнет разработчик, судорожно сжимая в руках стакан с рафом. — «Это всё красиво только на бумаге. А ты попробуй так на продакшене, где твое приложение сложнее чем To-Do лист! Да и никто не отменял концепцию контейнерных компонентов! Нам ведь нужно где-то это всё хранить!».
Удобная позиция, скажу я вам. Оправдывать раздутые компоненты концепцией контейнеров. Но статус контейнера не дает вам лицензию на всеядность компонента. Когда ваш контейнер знает и про формат ответа API, и про бизнес-правила обработки данных, и про стейт веб-плеера — это уже не контейнер, а свалка.
И проблема здесь в ответственности. Уверен, вы точно слышали про SOLID. И про то, что значит первая буква из этой аббревиатуры: Single Responsibility (SRP). Единственная ответственность.
Но что это значит на самом деле? В этот момент я обычно слышу уверенный голос воображаемого читателя:
— «Проще некуда: код должен делать только что-то одно. Функция считает сумму — значит, она считает сумму. Компонент рисует кнопку — значит, он рисует кнопку»
Но здесь вы и попадаете в ловушку. Что же это за зверь такой — это «что-то одно»?
Представьте функцию getMapCoordinates, которая высчитывает положение узла на интерактивной карте. Внутри неё — настоящий ад из расчетов: она учитывает проекцию Меркатора, текущий зум, кривизну Земли, фильтрует соседние точки, чтобы они не накладывались друг на друга, и на выходе выдает массив экранных координат.
С точки зрения процессора эта функция делает тысячи операций. С точки зрения читателя, который считает «действия», она делает кучу вещей: и считает, и фильтрует, и преобразует. Но с точки зрения архитектуры это всё еще одна ответственность. Ответственность Картографа. Пока эта логика нужна только для того, чтобы правильно расположить точки на карте, она едина.
Мы привыкли считать «действия», а надо считать «причины».
Давайте заглянем в то, что писал Роберт Мартин. Его определение SRP традиционно формулируется так:
«У модуля должна быть одна и только одна причина для изменения».
Но так как причины всегда исходят от людей, он перефразировал это точнее:
«Модуль должен отвечать за одного и только за одного актора».
Актор — это группа лиц (заказчиков), желающих изменения.
Здесь важно не путаться. Актор — это не конкретный Иван Иванович, который платит вам зарплату. Это - роль, социальная структура или группа лиц (заказчиков, пользователей или даже смежных отделов), у которых есть общие цели и которые являются источником требований к изменениям.
Вернемся к нашей getMapCoordinates. Проекция Меркатора, зум, кривизна Земли — это всё математика картографии. Если завтра географы решат, что Земля на самом деле в форме чемодана, придет Картограф и попросит изменить расчеты. У этой функции только одна причина для изменения — воля Картографа. Поэтому, несмотря на сложность, у неё одна ответственность.
Зачем я всё это рассказываю? Чтобы мы вернулись к базе: ответственность React — это UI.
Мы не должны отдавать ему бизнес-логику, математику или сложную работу с браузерными API. Почему? Потому что как только вы впихиваете это в компонент, вы создаете модуль, у которого слишком много причин для изменения.
Давайте возьмем ещё один пример. Представьте компонент видеоплеера. В нем перемешаны:
Логика взаимодействия с прогресс-баром (нужна Дизайнеру). Это то, как ползунок следует за курсором и анимируется — его визуальное поведение.
Аналитика просмотра (нужна Маркетологу). Условия, при которых просмотр считается засчитанным.
Работа с буфером и сетевыми чанками (нужна Бэкендерам).
Когда вы перемешиваете этих акторов внутри одного файла, происходит самое страшное — High Coupling (высокая связанность). Код аналитики начинает зависеть от того, как обновляется стейт таймера. Логика сети вшивается в useEffect, который срабатывает при клике на паузу.
В итоге:
Надежность падает, связанность растёт: исправляя баг в аналитике, вы ломаете перемотку.
Переносимость исчезает: вы не можете вытащить плеер в другой проект, не прихватив с собой тонну специфического «мусора».
Метрика «маты в минуту» (М/мин) зашкаливает.
Вы наверняка слышали, что плохая архитектура губительна для бизнеса: он начинает тратить всё больше времени разработчиков (а время — это деньги) на внедрение простейших фич или бесконечное исправление багов. И это чистая правда. Но дело здесь даже не в мифической «заботе о бизнесе». Если вам плевать на бизнес — плевать, имеете право. Но позаботьтесь о себе. Перемешивая ответственности, вы добровольно соглашаетесь поседеть раньше времени. Мы же хотим писать код с удовольствием, а не в режиме сапера на минном поле?
Как лечить?
Решение проблемы SRP всегда одинаково: делегирование. Но я знаю, что вы скажете: «Если я буду выносить каждую мелочь в отдельный файл, мой проект превратится в лабиринт».
Чтобы не плодить сущности зря, опирайтесь на два понятия: Coupling (связанность) и Cohesion (сцепленность). Вот ваш чек-лист для принятия решения:
Разделение по актору. Если в одном файле перемешаны интересы Дизайнера (анимации) и Маркетолога (аналитика) — они должны быть разделены логически.
Cohesion. Вы можете держать логику разных акторов в одном файле, пока это удобно. Но как только читать файл становится больно, а когнитивная нагрузка при поиске нужной строчки растет — пора разносить код по разным файлам.
Coupling. Когда вы выносите код, спросите себя: «Насколько сильно этот новый модуль будет дергать другие?». Идеально, если вы можете прочитать вынесенный код и понять его смысл, не прыгая обратно в родительский компонент.
Тест на удаление. Спросите себя: «Если завтра актор попросит вырезать эту фичу полностью, смогу ли я сделать это безболезненно?». Если для удаления аналитики вам нужно перелопатить половину реализации плеера и зачистить десятки связей — ваш код спроектирован плохо.
По итогу ваш компонент должен стать ленивым менеджером. Он не должен знать, как работает Audio API или как считаются координаты. Он должен просто говорить: «Эй, Картограф, дай мне точки», и «Эй, Плеер, нажми на паузу».
Заключение
Конечно, проблема нарушения SRP — это не эксклюзивная проблема Реакта. Это фундаментальная проблема любого кода. Но именно в React-сообществе она приобрела масштаб эпидемии.
React прямо говорит нам: «Я рисую картинки». Это его философия, его контракт с вами. Выбирая эту библиотеку, вы обязаны ��ринимать этот подход, а не заставлять его вести бухгалтерию, настраивать спутники и гадать на кофейной гуще аналитики.
React слишком прост, и эта простота — коварная ловушка. Она позволяет годами игнорировать классическое проектирование, прикрываясь «декларативностью», и нарушать те самые принципы, которые создатели закладывали в фундамент библиотеки.
Я всё понимаю. Мы живем не в вакууме. В пылу боя, когда бизнес требует вчера, рука сама тянется написать очередной useEffect и закрыть задачу за 5 минут. В аутсорсе, где код — это просто товар на развес, на такие вещи часто плевать. Оно и понятно.
Вы можете продолжать делать так, как делали. В конце концов, кто я, чёрт возьми, такой, чтобы указывать вам, как писать код? Но приведу аналогию из мира спорта: если вы тянете штангу с кривой спиной, всё, чему вы учитесь — это тянуть штангу с кривой спиной.
Важное предостережение: не воюйте с кодом, воюйте со сложностью. Чистая архитектура — это не когда у вас 1000 мелких файлов. Это когда вы, открыв любой из них, за 10 секунд понимаете, за что он отвечает и кто его «актор». Ваша итоговая цель — Low Coupling (низкая связанность) и High Cohesion (высокая сцепленность).
Выделяйте время на гигиену. Тренируйте «нюх» на запахи кода. Ведь в конечном итоге: хороший код — это код, который приятно удалять.
