Ну Mobx не то чтобы сложен. Пишем в обычном стиле где сторы и ViewModel - это классы, добавляем makeAutoObservable - получаем автоматический точечный ререндер компонентов при изменении только тех свойств, которые используются в компоненте.
Но тут согласен, от команды многое зависит - если совсем никто не пользовался, то обучение будет дольше, а если хоть один - то очень быстро других обучит.
А вот чем CSS Modules и scss не угодили?
Модули позволяют исключить пересечение стилей между компонентами, дают быстрый переход на место объявления, позволяют настраивать именование класса (например включать путь src-components-layouts-layoutWrapper), что создает ясный идентификатор для компонентов. Для e2e тестов не подойдет, но для дебага - прекрасно, сразу видно кнопку в каком из 100 компонентов, использующих кнопки, нажал пользователь и получил ошибку. Также при желании можно настроить генерацию d.ts файлов из стилей, чтобы найти все неиспользуемые классы (на постоянной основе - нежелательно, но в качестве плановой оптимизации раз в 3 месяца - отлично).
Scss - это миксины, nesting (вложение одних классов в другие), математика и циклы, удобные импорты. Не обязательно использовать именно препроцессоры - можно использовать сразу постпроцессоры (PostCSS) с соответствующим интерпретатором. Во многих проектах постпроцессор и так используется (для автопрефикса и оптимизации например), и добавление scss не сильно ухудшит перфоманс сборки, но добавит удобный DX.
Ну, про FSD, TanStack Router, TanStack Query и Effector я согласен с предыдущим комментатором, что сейчас это самое неэффективное, и это никак не связано с годом их выпуска. Но популярных альтернатив лучше и правда мало, и если сжатые сроки - то можно и затянуть в проект, особенно если команда хорошо с ними знакома.
Спасибо, перешел в одном проекте с Prettier на него - скорость полного линтинга сократилась с 8.5с до 7с. Порадовало количество настроек и их гибкость, однако для совпадения с предыдущим форматированием через Prettier пришлось детально понастраивать - только лишь stylistic.configs.customize не дало нужного результата.
В Реакте действительно особое поведение для контролируемых инпутов, я привел в статье ссылку на issue, где обсуждается этот вопрос и приводятся десятки вариантов решения. Там довольно подробно описывали сложности реализации, например здесь
У Реакта другая система реактивности - изменения Preact signals не будут вызывать ререндер компонента, а если с адаптером - то все равно не будет точечных апдейтов без ручной оптимизации. То есть на мой взгляд привязка к Preact будет достаточно сильная.
Для осуществления глубокой реактивности (не только примитивов), как вы правильно заметили, тоже будет необходима дополнительная библиотека, и по размеру код думаю приблизится к Preact+MobX, учитывая все нюансы.
Не спорю, что если использовать только 1 фреймворк, то можно оптимизировать код, однако мне было интересно именно side-to-side сравнение, поэтому взял MobX для связки с Preact.
Да, есть немало библиотек, в том числе для Solid, которые решают эту проблему. Но если в приложении пара инпутов и не хочется раздувать размер бандла, то в контролируемых инпутах Реакта все решается намного проще, чем в неконтролируемых Солида.
Для Реакта я мог использовать простые regexp для запрета ввода некорректных символов, теперь же приходится все делать через сторонние библиотеки. Не то, чтобы это вызывало серьезные неудобства, но отсутствие привычного функционала портит впечатление от фреймворка.
Да, меня тоже смущает, что Painting отличается на 50%. Думаю, тут дело в ререндерах и отсутствии оптимизации для Реакта (если бы выносил больше компонентов и они точечно обновлялись - то меньше бы было), об этом написал в статье.
Page1 - Page 2 не реактивные, а прод-приложение, из которого приводил метрики - реактивные, там есть изменения в структуре разметки, динамические элементы, показы по условию, изменения стилей и т.п. Делать что-то сложное в Реактосолиде я не стал - просто сгенерировал статику через AI и убрал 80% архитектуры прод-приложения. Можно самостоятельно для интереса сделать что-то более сложное - думаю, различия в перфомансе будут более явными.
Preact+Signals, к сожалению, совсем не поддерживает реактивные классы и завязывается на написание логики внутри компонента - этот подход не адаптировать под ViewModel и MobX. Перфоманс, уверен, будет лучше, но и ограничения явные - сильная привязка к Preact и невозможность быстрого переключения на React/Solid. Для меня же Solid был новым опытом, и оставить себе "путь отступления" было крайне важным на старте - хотя после написания проекта это оказалось избыточным, экосистемы и возможностей Solid хватило с лихвой.
Некст вне контекста статьи, как и мегабайты картинок. Контролируемые инпуты нужны для фильтрации того, что вводит пользователь. Попробуйте на нативном инпуте нативно обработать oninput, onchange, onpaste, onkeypress, onkeydown c учетом мобильных браузеров и всех версий операционки и разрешить скажем ввод "a-b". Это очень нетривиальная задача, если у вас в наличии десятки устройств.
Добавьте обработку маски и позицию каретки для удобного редактирования.
Мне была важна возможность перехода на React/Preact+MobX если не хватит экосистемы, поэтому детали реактивности были не важны - главное, чтобы была возможность делать реактивные инстансы классов по аналогии с MobX. Что под капотом - геттеры-сеттеры, прокси или сигналы не так важно, если они дают достаточный перфоманс и не налагают серьезных ограничений.
Конечно, есть фреймворки, предлагающие менее 5.78 kb на старте, но не с jsx синтаксисом и без возможности практически безболезненно переключаться на другие фреймворки.
Если вы это серьезно, то крайне недооцениваете классическую структуру.
Несколько "связанных модалок" - это несколько "независимых модалок, вызываемых колбеками при закрытии со статусом успех". Эти модалки лежат в components/modal/lib и могут вызывать друг друга. Если они вызываются только с одной страницы - то будут лежать в папке страницы.
Что за modules (модули) в замену семантичным pages не понимаю. Звучит как заменим "страницу" на "модуль" - как это можно вообще придумать?
Показ баннера - компонент лежит в components/banner, вызывается параметром в глобальном сторе showBanner: boolean. Этот компонент смотрит в глобальный стор и при изменении значения отображается. Он не зависит от страниц.
"кнопка которая меняет дизайн в зависимости от наличия подписки" - интересный кейс, то есть если передал onClick то один вид, если не передал - то другой. Решается маппером в компоненте components/button. Если ей нужен значок - он передается через проп <Button leftIcon={''} rightIcon={''} />, никакой необходимости делать дубляжи компонентов на каждый проп нет.
Функции форматирования строк складываются в utils/formatString, которая экспортирует объект с 100 разными видами форматирования, если это нужно проекту. Это по факту исключает дубляж - глазами пробежаться по методам куда проще, чем искать в море widgets, entity, app, feature и т.п. разнородных файлов с форматированиями.
Нападки в целом "из пальца", попробуйте сами в классике писать - там нет перечисленных проблем)
хорошо структурированный и понятный проект позволет лучше его понимать, и соотвественно его и рефакторить проще снижая техдол, и от дублирования кода избавлятся, и переосмысливать связи между компонентами. Все эти шаги проще делать при разбиении по доменам
Вы про это? Ну, я не поддерживаю Клерика в плане, что микрофронты - хорошее решение, но в остальном согласен с его позицией.
И, к сожалению, вашу позицию про FSD как способ "хорошо структурировать проект по доменным областям", который решает описанные в цитате проблемы - не могу поддержать.
В классике есть домены - это страницы src/pages/somePage. Это понятная и хорошо структурированная система, синхронизированная с роутингом. Рефакторить очень просто, риск дубляжа сведен к минимуму за счет глобальных слоев, связи между слоями достаточно четкие.
Проблема "свалки в коде" существует в классике, это верно, но она возникает только когда проект перерастает продвинуто-средний размер (30+ страниц, 50+ компонентов, 70+ вызовов апи). И для решения этой проблемы масштабируемости есть ряд способов - микрофронты это один вариант, как описано выше, монорепа с выделением ряда страниц - другой вариант, здесь все зависит от архитектора. Возможен и вариант развития классики "в глубину" - сгруппировать страницы по папкам, выделив слои внутри них.
Но с такими большими проектами редко сталкиваешься, а если да - то там достаточно опыта для наиболее грамотного выбора структуры, подходящей проекту.
Проблема наименований в роутере и в проекте - в проекте 200 страниц, из них нужно сгруппировать 10, и это не ложится в термин "страница". Здесь ключевое - "нужно сгруппировать", то есть пришла некая задача от бизнеса или архитектора - эти страницы нужно куда-то выделить. Очевидное решение - в новый app, то есть приложение, содержащее эти страницы + всю необходимую для них логику. Термин "приложение" не противоречит классической структуре, и верно, что "страницей" его называть не правильно. Но в контексте роутера это - набор страниц, роутер не должен знать о структуре и фреймворках, используемых в отдельных "приложениях", из которых формируется единое "приложение". Роутер существует в едином контексте - это вкладка браузера и URL, а откуда берутся компоненты для страниц - это детали реализации. Поэтому я не вижу здесь логических ошибок - роутинг и структура проекта это разные вещи.
Да, есть проекты из 1 страницы - лендинги или "виджеты", которые встраиваются в сторонние сайты и например отображают погоду. Они тоже хорошо ложатся на классическую структуру - будет 0 страниц (только корневой App) и набор компонентов, которые там отображаются, и не будет слоя роутинга.
Про микрофронтенды я написал в контексте треда. Я согласен, что они по большей части неэффективны и избыточны, а для работы разных команд над одним проектом часто достаточно монорепы и организационных ограничений. По крайней мере до 5 команд на моем опыте комфортно уживались без существенных блоков для других команд в монорепе. А вот аналогичный опыт с разделением по микрофронтам был очень негативным, так что я тоже использовал бы их только в крайнем случае. Написал пример для микрофронтов в комменте выше только для того, чтобы показать, что классическая структура не мешает их выделить.
В целом вы видимо хотите перевести тред в тему "нужны ли микрофронты", но для этого есть куча статей о них, я по крайней мере там выступаю как противник данного подхода. Здесь же обсуждается структура папок и взаимосвязей, немного другой контекст
Но это будет немного не то, что в классике, думаю. Там глобальный слой апи могут вызывать все, кто хочет получить эти данные, они будут сложены в глобальный стор. Компонент всегда лежит в src/components, не нужно его искать по entity/ui, widgets/ui/ui/ui. features/ui/ui/ui/ui. Условно, классика - это все shared (components), пока не потребуется положить в page. Один из нас думает локальностью и изолированностью, другой - глобальностью и что нужно класть в модуль только при необходимости) У обоих разные проблемы, но в целом одинаковые
Ключевое - роутинг. У вас в проекте за него отвечает отдельный фреймворк, но это ключевая часть - синхронизировать урл и отображение. Второе - автоматизация, это хоть и невидимая часть в вашем проекте, но играет существенную роль (от сборщика до скрытой логики). Третье - организация стилей, которая подчиняется своим правилам каскадности и CSSOM и строится по отдельному дереву. Четвертое - очевидное - иерархия взаимодействия, чего нет в FSD, насколько знаю, там в основном все через хаки. Ну есть еще несколько моментов)
Как разработчика меня интересуют только метрики профайлера + структура в дев тулз + легкий поиск из html конкретного компонента + достаточно емкие логи в Sentry, чтобы найти все баги за "поиск по проекту". Не столь важно различие, как мы кладем папки и файлы и их связываем.
Но я же не об этом говорил в комменте, а указал, что вы в ряде утверждений ввели в заблуждение относительно содержания статьи, триггернули человека, захейтили и унизили за то, что показал на это - возможно, немного грубым способом. Но это же ваша статья и ваша аудитория разработчиков, причем опытных. Я тоже так в своих статьях поначалу поступал, за что даже себе минус в карму пытался поставить) Затем, как и вы, старался вывести на конструктивный диалог - а если не получалось, то не оскорблял.
У меня есть старый бойлерплейт https://github.com/dkazakov8/dk-boilerplate , в нем классическая структура является основой слоистой архитектуры. Боюсь, разговор об архитектурах не впишется в формат комментов, это слишком большая тема)
И вам спасибо за качественный ответ по всем пунктам)
Я бы тоже перевел ваш проект на классику и показал преимущества, однако для ближайшей статьи запланировал библиотеку роутинга, лишенную всех проблем React Router и Tanstack. Лет 8 ей занимался, но для вашего проекта она не будет полезна - стек уже выбран)
Значит, существенную дискуссию переведем под следующую вашу статью. К слову, не путайте архитектуру и структуру, FSD - это лишь структура папок и их взаимосвязей, маленькая часть архитектуры)
В статье нет ни цифр, ни реальных итогов - только про то, как вы вышли на чуть лучшие показатели спустя какое-то время в конкретных условиях конкретной компании и состава ее команды, выбрав с нуля FSD, и сравнивая только со своим перфомансом. Сравнения с не-fsd не было - уже думаю десятый раз читаю статью, но кроме "документацию навряд ли так легко бы написали, если бы fsd не описал для нас" не вижу, при этом на свою документацию вы выбивали время завышенными сроками разработки.
"Традиционной" архитектурой обычно является ее отсутствие - нет. Наш FSD-проект безоговорочно лидирует по скорости погружения - нет. Мне тоже пора в палату по вашему мнению, или критично осмыслите то, что написали в комменте?)
Я плохо понимаю этот специфичный язык - "сущность", "обобщенные модели", "фичи", "свойства фичи", "бизнес-сущность", "юзер процесс". Я понимаю их значение на js - "сущность класса" в простом разговоре есть "инстанс класса", "модель" - TS-тип класса, "обобщенная модель" - в простом разговоре абстрактный класс. "фича" - процедурный функционал, "свойства фичи" возможно относится к алгоритму внутри функции или сайд-эффектам. "бизнес-сущность" обычно говорят, когда не знают, как описать что-то, но что-то комплексное, возможно включающее вызов апи и обработку ошибок. "юзер процесс" - форк системного процесса ноды.
Это мета-язык на основе js, который специфичен для структуры папок FSD - это я понимаю) но стоит ли настолько контекстуально делать аналоги понятий, и улучшает ли это понимаемость проекта?
В классике все просто и уникально:
Компонент - введенный много лет назад термин, означающий разметку + логику, вынесенную в отдельный класс или функцию. Это универсально в React, Angular, Vue, Solid, Web Components и других компонентных библиотеках. Аналога в js нет
Страница - семантичный комплексный элемент, существующий с зарождения веба. По любому URL из браузера открывается страница (конечно, если не из браузера - то другая история и другие протоколы). Она может содержать компоненты или быть цельной независимой html-compatible структурой. Но компоненты не могут содержать страницу в прямом смысле, только через iframe, то есть отдельный URL. Аналога в js нет
Сторы - это либо Data store, который содержит в себе только данные, либо Model/Service/UI/ViewModel Store, которые могут содержать методы. В классике выбирают наименование, которое им более привычно, но все равно эти 2 типа как правило четко разделяются и присутствуют. Аналога в js нет
Действия - это имеющие доступ сторам и апи функции. Они могут быть частями Model/Service/UI Store, а могут быть отдельными. В ряде случаев их могут называть "контроллерами", они могут читать, писать, вызывать - делать все, что нужно для функционала. Аналога в js нет (ну кроме слова "функция", но оно очень общее)
Слои - это семантическое группирование по функционалу и свойствам. API-слой отвечает за получение данных из внешних источников, Reaction-слой (это могут быть хуки, реакции СТМ, эвенты) - за реагирование на сигналы, подаваемые компонентами либо страницами. Слой сторов - объяснил выше. Слой констант, слой утилит + в зависимости от архитектуры проекта. Слои могут быть глобальными (для всего приложения или для отдельного приложения мультирепы), могут быть постраничными (в ряде интерпретаций - модульными) и могут быть локальными. В классике всегда соблюдается принцип "низшие знают о высших, но высшие не знают о низших". Нереально редкие исключения, о которых я даже не могу предположить, чтобы что-то глобальное знало о локальном, решаются через события: LocalUiStore -> call event ('someName', payload) -> GlobalReactionStore -> get event ('someName', payload) -> setData to GlobalStore. Но в классике такого не бывает, чисто теория
Если в контексте вашего ответа, то "бизнес-сущность продукт" - это компонент src/components/Product. "следующие сущности: "карточка маленькая", "карточка большая", "список продуктов" решаются либо пропом к компоненту, либо маппером, когда в src/components/Product лежат ProductSmall.tsx, ProductBig.tsx, ProductList.tsx, хотя никто бы не стал список продуктов помещать в один компонент.
Авторизация в классике - это не "фича" в контекстуальном понимании FSD, это действие. Оно вызывает апи с параметрами и либо выдает нотификацию, либо кладет в стор юзера. Этот экшен могут вызывать как компоненты, так и страницы. В ряде архитектур могут и Model/Service/UI Store. "оплата", или "добавление в корзину", или "удаление из корзины" - то же самое. И в действии могут быть "апи и, например, оптимистичная мутация данных на фронте".
В целом классика максимально всеобъемлюща, уникальна для структуры папок, не пересекается с js по неймингу. Но она требует, как и FSD, определенного опыта для организации. Но документации для нее по факту нет, поэтому FSD в этом плане выигрывает. Но в большинстве случаев документация и не нужна, так как фронтендеры и так мыслят этими сущностями, и классика есть отражение логичного мышления.
Скажу сразу, с FSD в проде я работал только единожды (в остальном только с классической структурой), все что здесь напишу - мысленные эксперименты + небольшой опыт.
"Изолированность и модульность", включающая в себя удобства в виде "независимая от других разработка модуля одним разработчиком", "изоляция кода и багов", "масштабируемость".
Это звучит безусловно непротиворечиво - каждый пишет код в своей папке, возможно со своим стилем кода, несет за него ответственность и это вдохновляет писать качественно. Однако налицо и катастрофический недостаток в виде дубляжа. 5 разработчиков на своих 5 страницах делают одно и то же обращение к апи, модели запроса-ответа, реализацию обработки ошибок со своими текстами (условно - понадобилось стянуть справочник Cities с бэка). Также они пишут свои кнопки, свои формы, свою локализацию, свои интерфейсы к компонентам.
В ряде случаев все это будет уникальным для страницы / виджета / фичи, а в ряде случаев (предположу что 30%) это будет дубляжом. Соответственно если апи бэка изменился для этой ручки - то нужно менять в 5 модулях, если кнопку нужно перестилить или добавить состояние загрузки - тоже в 5 модулях нужно искать, разбираться в каждой реализации и делать уникальные решения для каждой уникальной, но похожей, кнопки.
В итоге на этапе разработки ресурс времени разработчиков экономится, однако в перспективе поддержки из-за неединообразия в реализации, но единообразия в функционале, выходит кратное возрастание затрат времени.
Класть компоненты в shared/ui для переиспользуемости - безусловно частично выправляет ситуацию. Однако с этим теряется и "модульность" - при разработке опять же 5 разрабам нужно добавить разный функционал в кнопку (иногда stopPropagation, иногда внезапно появившийся новый вариант цвета, иногда дополнительную анимацию). Они начинают "толкаться" в одном компоненте и в определенных проектах (например с релизным циклом в 1 день) решают ее скопировать в свой модуль, дополнительно увеличивая дубляж, так как решать все потенциальные мердж-конфликты заранее с другими разрабами и их локальными ветками через чат - это пытка.
Потом, разумеется, если разработка активная, факт дубляжа забывается, а через год разраб, делающий редизайн, видит десятки похожих кнопок с разной реализацией. Это история из жизни - как раз делал редизайн в таком проекте, который из-за начального решения следовать принципу изоляции начал реализовывать это через дубляж.
У вас идеально скомплектованная команда и хорошие процессы - перед стартом фронт-задачи есть ТЗ, дизайн, готовый бэк и время на анализ каждой задачи с командой + достаточное время на реализацию + есть время писать техническую документацию и обосновывать принятые решения + время на декомпозицию и слежение за статусами подзадач. К сожалению, таких компаний не так много, и часто есть только грубое ТЗ и примерный дизайн, и за 3 дня нужно сделать одновременно и параллельно дизайн + фронт + бэк и корректно интегрироваться, да и в процессе работы над задачей требования меняются вплоть до последнего часа. При этом бизнес очень жестко настаивает, что "там дел на 5 минут".
Это часто встречается в стартапах, которым за пару месяцев нужно сделал свой Гугл (а им всем нужно) и сделать это лучше, чем у конкурентов. Здесь на мой взгляд выиграет FSD за счет изоляции, если разрабов 3+, и проиграет классике, если меньше - классика заточена под переиспользуемость и глобальность + четкую структуру слоев, и из-за динамичных изменений и быстрой разработки проще поправить в 1 месте для всех 10 страниц, чем в каждом из них, так как нет достаточных ресурсов разработчиков.
Также вы говорили, что уделите основное внимание "процессу разработки на FSD", но большая часть статьи - про Agile (управление задачами, ретроспектива, распределение задач, обсуждение с коллегами, документирование, разбиение на итерации) - это универсальные принципы, никак не зависящие от структуры проекта, и те же самые этапы по Agile должны быть везде. В том числе в классике было бы то же самое (только с меньшей детализацией) при наличии таких же ресурсов и команды, только вместо обсуждений "куда положить то или иное и как назвать" больше бы обсуждались детали реализации (дебаунсы, стоит ли вынести функционал на слой выше для переиспользуемости, обсуждение с бэком удобного контракта апи).
В итогах вы тоже сравниваете сами с собой - насколько команда привыкла к структуре, насколько вначале было неэффективное разбиение задач, как вы постепенно улучшали структуру проекта и за счет "притирки" (давали бизнесу прежние сроки, а делали за меньшее время) высвободили время для рефакторинга и документации. Которая, к слову, в классике пишется очень просто, хотя ее по факту никто не читает - классика по большей части самодокументируется семантикой и слоями без искусственного разделения.
То есть статья получилась не про FSD, а про то, как хорошо когда в компании много ресурсов и времени, и можно пробовать что угодно, и все будет хорошо)
P.S. я делал похожий проект, только в соло и намного сложней (так как нужно было еще 3 проекта параллельно делать - приложения для сборщиков и курьеров, для мониторинга сотрудников, клиентский сайт) - на классическую структуру это все прекрасно ложится за счет ее структурированности и переиспользуемости, и все, что увидел в статье, никак не сподвигло бы на переход. Если нужно, могу рассказать о структуре этих страниц, но это вряд ли нужно - в классике и так все разбираются, поэтому и статей по ней нет.
Конечно, Button может получать данные наиболее удобным способом - через глобальный контекст, через прямой импорт глобального стора, через пропсы. Когда может быть удобнее использовать глобальный стор, а не пропсы?
Например, кнопка читает глобальную тему стилей и нужно на лету менять цвет всех кнопок на проекте. Допустим, что css variables не используются, а тема хранится в глобальном сторе или передается как во многих UI библиотеках через верхнеуровневый контекст, либо css in js. В этом случае безусловно через пропсы каждой кнопке передавать текущий цвет нелогично.
Второй пример - локализация. Есть 10 языков, данные текстов хранятся в глобальном сторе. Button сделали с таким интерфейсом, что текст опционален - `<Button text={store.localize('Ok')} />`, а по дефолту кнопка имеет текст Ok. Это часто используется например в модалках или конфермах, когда есть 2 кнопки - отказ и принять, и не хочется каждый раз тексты передавать через пропы. Разумеется, кнопка возьмет дефолтный текст из глобального стора, если хранение локализации находится там.
Третий пример - есть специфичная кнопка src/components/ButtonGetUser, которая используется на 5 страницах и должна вызвать апи получения данных по пользователю, и вызвать модальное окно с показом его данных. Этот кейс ближе к админкам. Зачем дублировать логику, передавая в onClick одну и ту же логику, если этот компонент может сам обратиться к глобальному апи и глобально вызвать модалку - DRY принцип в действии.
Я не задумываюсь о разделении "глупый компонент", "тупой компонент", "компонент, принимающий все только через пропы", "компонент, способный сам вызывать глобальные действия" - если того требует логика, компонент может трансформироваться между этими состояниями без лишней когнитивной нагрузки и выделения дополнительных сущностей - адаптеров, коннекторов, контроллеров, HOC, виджетов и что только не видел еще для "соблюдения неких принципов".
Про декорирование не понял, я вроде о нем ничего не говорил)
Ну Mobx не то чтобы сложен. Пишем в обычном стиле где сторы и ViewModel - это классы, добавляем makeAutoObservable - получаем автоматический точечный ререндер компонентов при изменении только тех свойств, которые используются в компоненте.
Но тут согласен, от команды многое зависит - если совсем никто не пользовался, то обучение будет дольше, а если хоть один - то очень быстро других обучит.
А вот чем CSS Modules и scss не угодили?
Модули позволяют исключить пересечение стилей между компонентами, дают быстрый переход на место объявления, позволяют настраивать именование класса (например включать путь
src-components-layouts-layoutWrapper
), что создает ясный идентификатор для компонентов. Для e2e тестов не подойдет, но для дебага - прекрасно, сразу видно кнопку в каком из 100 компонентов, использующих кнопки, нажал пользователь и получил ошибку. Также при желании можно настроить генерацию d.ts файлов из стилей, чтобы найти все неиспользуемые классы (на постоянной основе - нежелательно, но в качестве плановой оптимизации раз в 3 месяца - отлично).Scss - это миксины, nesting (вложение одних классов в другие), математика и циклы, удобные импорты. Не обязательно использовать именно препроцессоры - можно использовать сразу постпроцессоры (PostCSS) с соответствующим интерпретатором. Во многих проектах постпроцессор и так используется (для автопрефикса и оптимизации например), и добавление scss не сильно ухудшит перфоманс сборки, но добавит удобный DX.
Ну, про FSD, TanStack Router, TanStack Query и Effector я согласен с предыдущим комментатором, что сейчас это самое неэффективное, и это никак не связано с годом их выпуска. Но популярных альтернатив лучше и правда мало, и если сжатые сроки - то можно и затянуть в проект, особенно если команда хорошо с ними знакома.
Спасибо, перешел в одном проекте с Prettier на него - скорость полного линтинга сократилась с 8.5с до 7с. Порадовало количество настроек и их гибкость, однако для совпадения с предыдущим форматированием через Prettier пришлось детально понастраивать - только лишь
stylistic.configs.customize
не дало нужного результата.В Реакте действительно особое поведение для контролируемых инпутов, я привел в статье ссылку на issue, где обсуждается этот вопрос и приводятся десятки вариантов решения. Там довольно подробно описывали сложности реализации, например здесь
У Реакта другая система реактивности - изменения Preact signals не будут вызывать ререндер компонента, а если с адаптером - то все равно не будет точечных апдейтов без ручной оптимизации. То есть на мой взгляд привязка к Preact будет достаточно сильная.
Для осуществления глубокой реактивности (не только примитивов), как вы правильно заметили, тоже будет необходима дополнительная библиотека, и по размеру код думаю приблизится к Preact+MobX, учитывая все нюансы.
Не спорю, что если использовать только 1 фреймворк, то можно оптимизировать код, однако мне было интересно именно side-to-side сравнение, поэтому взял MobX для связки с Preact.
Да, есть немало библиотек, в том числе для Solid, которые решают эту проблему. Но если в приложении пара инпутов и не хочется раздувать размер бандла, то в контролируемых инпутах Реакта все решается намного проще, чем в неконтролируемых Солида.
Для Реакта я мог использовать простые regexp для запрета ввода некорректных символов, теперь же приходится все делать через сторонние библиотеки. Не то, чтобы это вызывало серьезные неудобства, но отсутствие привычного функционала портит впечатление от фреймворка.
Да, меня тоже смущает, что Painting отличается на 50%. Думаю, тут дело в ререндерах и отсутствии оптимизации для Реакта (если бы выносил больше компонентов и они точечно обновлялись - то меньше бы было), об этом написал в статье.
Page1 - Page 2 не реактивные, а прод-приложение, из которого приводил метрики - реактивные, там есть изменения в структуре разметки, динамические элементы, показы по условию, изменения стилей и т.п. Делать что-то сложное в Реактосолиде я не стал - просто сгенерировал статику через AI и убрал 80% архитектуры прод-приложения. Можно самостоятельно для интереса сделать что-то более сложное - думаю, различия в перфомансе будут более явными.
Preact+Signals, к сожалению, совсем не поддерживает реактивные классы и завязывается на написание логики внутри компонента - этот подход не адаптировать под ViewModel и MobX. Перфоманс, уверен, будет лучше, но и ограничения явные - сильная привязка к Preact и невозможность быстрого переключения на React/Solid. Для меня же Solid был новым опытом, и оставить себе "путь отступления" было крайне важным на старте - хотя после написания проекта это оказалось избыточным, экосистемы и возможностей Solid хватило с лихвой.
Некст вне контекста статьи, как и мегабайты картинок. Контролируемые инпуты нужны для фильтрации того, что вводит пользователь. Попробуйте на нативном инпуте нативно обработать oninput, onchange, onpaste, onkeypress, onkeydown c учетом мобильных браузеров и всех версий операционки и разрешить скажем ввод "a-b". Это очень нетривиальная задача, если у вас в наличии десятки устройств.
Добавьте обработку маски и позицию каретки для удобного редактирования.
Другая экосистема, другой хаб для дискуссии)
Мне была важна возможность перехода на React/Preact+MobX если не хватит экосистемы, поэтому детали реактивности были не важны - главное, чтобы была возможность делать реактивные инстансы классов по аналогии с MobX. Что под капотом - геттеры-сеттеры, прокси или сигналы не так важно, если они дают достаточный перфоманс и не налагают серьезных ограничений.
Конечно, есть фреймворки, предлагающие менее 5.78 kb на старте, но не с jsx синтаксисом и без возможности практически безболезненно переключаться на другие фреймворки.
Если вы это серьезно, то крайне недооцениваете классическую структуру.
Несколько "связанных модалок" - это несколько "независимых модалок, вызываемых колбеками при закрытии со статусом успех". Эти модалки лежат в components/modal/lib и могут вызывать друг друга. Если они вызываются только с одной страницы - то будут лежать в папке страницы.
Что за modules (модули) в замену семантичным pages не понимаю. Звучит как заменим "страницу" на "модуль" - как это можно вообще придумать?
Показ баннера - компонент лежит в components/banner, вызывается параметром в глобальном сторе showBanner: boolean. Этот компонент смотрит в глобальный стор и при изменении значения отображается. Он не зависит от страниц.
"кнопка которая меняет дизайн в зависимости от наличия подписки" - интересный кейс, то есть если передал onClick то один вид, если не передал - то другой. Решается маппером в компоненте components/button. Если ей нужен значок - он передается через проп <Button leftIcon={''} rightIcon={''} />, никакой необходимости делать дубляжи компонентов на каждый проп нет.
Функции форматирования строк складываются в utils/formatString, которая экспортирует объект с 100 разными видами форматирования, если это нужно проекту. Это по факту исключает дубляж - глазами пробежаться по методам куда проще, чем искать в море widgets, entity, app, feature и т.п. разнородных файлов с форматированиями.
Нападки в целом "из пальца", попробуйте сами в классике писать - там нет перечисленных проблем)
Вы про это? Ну, я не поддерживаю Клерика в плане, что микрофронты - хорошее решение, но в остальном согласен с его позицией.
И, к сожалению, вашу позицию про FSD как способ "хорошо структурировать проект по доменным областям", который решает описанные в цитате проблемы - не могу поддержать.
В классике есть домены - это страницы src/pages/somePage. Это понятная и хорошо структурированная система, синхронизированная с роутингом. Рефакторить очень просто, риск дубляжа сведен к минимуму за счет глобальных слоев, связи между слоями достаточно четкие.
Проблема "свалки в коде" существует в классике, это верно, но она возникает только когда проект перерастает продвинуто-средний размер (30+ страниц, 50+ компонентов, 70+ вызовов апи). И для решения этой проблемы масштабируемости есть ряд способов - микрофронты это один вариант, как описано выше, монорепа с выделением ряда страниц - другой вариант, здесь все зависит от архитектора. Возможен и вариант развития классики "в глубину" - сгруппировать страницы по папкам, выделив слои внутри них.
Но с такими большими проектами редко сталкиваешься, а если да - то там достаточно опыта для наиболее грамотного выбора структуры, подходящей проекту.
Я попытаюсь разложить для себя и ответить.
Проблема наименований в роутере и в проекте - в проекте 200 страниц, из них нужно сгруппировать 10, и это не ложится в термин "страница". Здесь ключевое - "нужно сгруппировать", то есть пришла некая задача от бизнеса или архитектора - эти страницы нужно куда-то выделить. Очевидное решение - в новый app, то есть приложение, содержащее эти страницы + всю необходимую для них логику. Термин "приложение" не противоречит классической структуре, и верно, что "страницей" его называть не правильно. Но в контексте роутера это - набор страниц, роутер не должен знать о структуре и фреймворках, используемых в отдельных "приложениях", из которых формируется единое "приложение". Роутер существует в едином контексте - это вкладка браузера и URL, а откуда берутся компоненты для страниц - это детали реализации. Поэтому я не вижу здесь логических ошибок - роутинг и структура проекта это разные вещи.
Да, есть проекты из 1 страницы - лендинги или "виджеты", которые встраиваются в сторонние сайты и например отображают погоду. Они тоже хорошо ложатся на классическую структуру - будет 0 страниц (только корневой App) и набор компонентов, которые там отображаются, и не будет слоя роутинга.
Про микрофронтенды я написал в контексте треда. Я согласен, что они по большей части неэффективны и избыточны, а для работы разных команд над одним проектом часто достаточно монорепы и организационных ограничений. По крайней мере до 5 команд на моем опыте комфортно уживались без существенных блоков для других команд в монорепе. А вот аналогичный опыт с разделением по микрофронтам был очень негативным, так что я тоже использовал бы их только в крайнем случае. Написал пример для микрофронтов в комменте выше только для того, чтобы показать, что классическая структура не мешает их выделить.
В целом вы видимо хотите перевести тред в тему "нужны ли микрофронты", но для этого есть куча статей о них, я по крайней мере там выступаю как противник данного подхода. Здесь же обсуждается структура папок и взаимосвязей, немного другой контекст
Но это будет немного не то, что в классике, думаю. Там глобальный слой апи могут вызывать все, кто хочет получить эти данные, они будут сложены в глобальный стор. Компонент всегда лежит в src/components, не нужно его искать по entity/ui, widgets/ui/ui/ui. features/ui/ui/ui/ui. Условно, классика - это все shared (components), пока не потребуется положить в page. Один из нас думает локальностью и изолированностью, другой - глобальностью и что нужно класть в модуль только при необходимости) У обоих разные проблемы, но в целом одинаковые
Ключевое - роутинг. У вас в проекте за него отвечает отдельный фреймворк, но это ключевая часть - синхронизировать урл и отображение. Второе - автоматизация, это хоть и невидимая часть в вашем проекте, но играет существенную роль (от сборщика до скрытой логики). Третье - организация стилей, которая подчиняется своим правилам каскадности и CSSOM и строится по отдельному дереву. Четвертое - очевидное - иерархия взаимодействия, чего нет в FSD, насколько знаю, там в основном все через хаки. Ну есть еще несколько моментов)
Как разработчика меня интересуют только метрики профайлера + структура в дев тулз + легкий поиск из html конкретного компонента + достаточно емкие логи в Sentry, чтобы найти все баги за "поиск по проекту". Не столь важно различие, как мы кладем папки и файлы и их связываем.
Но я же не об этом говорил в комменте, а указал, что вы в ряде утверждений ввели в заблуждение относительно содержания статьи, триггернули человека, захейтили и унизили за то, что показал на это - возможно, немного грубым способом. Но это же ваша статья и ваша аудитория разработчиков, причем опытных. Я тоже так в своих статьях поначалу поступал, за что даже себе минус в карму пытался поставить) Затем, как и вы, старался вывести на конструктивный диалог - а если не получалось, то не оскорблял.
У меня есть старый бойлерплейт https://github.com/dkazakov8/dk-boilerplate , в нем классическая структура является основой слоистой архитектуры. Боюсь, разговор об архитектурах не впишется в формат комментов, это слишком большая тема)
И вам спасибо за качественный ответ по всем пунктам)
Я бы тоже перевел ваш проект на классику и показал преимущества, однако для ближайшей статьи запланировал библиотеку роутинга, лишенную всех проблем React Router и Tanstack. Лет 8 ей занимался, но для вашего проекта она не будет полезна - стек уже выбран)
Значит, существенную дискуссию переведем под следующую вашу статью. К слову, не путайте архитектуру и структуру, FSD - это лишь структура папок и их взаимосвязей, маленькая часть архитектуры)
В статье нет ни цифр, ни реальных итогов - только про то, как вы вышли на чуть лучшие показатели спустя какое-то время в конкретных условиях конкретной компании и состава ее команды, выбрав с нуля FSD, и сравнивая только со своим перфомансом. Сравнения с не-fsd не было - уже думаю десятый раз читаю статью, но кроме "документацию навряд ли так легко бы написали, если бы fsd не описал для нас" не вижу, при этом на свою документацию вы выбивали время завышенными сроками разработки.
"Традиционной" архитектурой обычно является ее отсутствие - нет. Наш FSD-проект безоговорочно лидирует по скорости погружения - нет. Мне тоже пора в палату по вашему мнению, или критично осмыслите то, что написали в комменте?)
Я плохо понимаю этот специфичный язык - "сущность", "обобщенные модели", "фичи", "свойства фичи", "бизнес-сущность", "юзер процесс". Я понимаю их значение на js - "сущность класса" в простом разговоре есть "инстанс класса", "модель" - TS-тип класса, "обобщенная модель" - в простом разговоре абстрактный класс. "фича" - процедурный функционал, "свойства фичи" возможно относится к алгоритму внутри функции или сайд-эффектам. "бизнес-сущность" обычно говорят, когда не знают, как описать что-то, но что-то комплексное, возможно включающее вызов апи и обработку ошибок. "юзер процесс" - форк системного процесса ноды.
Это мета-язык на основе js, который специфичен для структуры папок FSD - это я понимаю) но стоит ли настолько контекстуально делать аналоги понятий, и улучшает ли это понимаемость проекта?
В классике все просто и уникально:
Компонент - введенный много лет назад термин, означающий разметку + логику, вынесенную в отдельный класс или функцию. Это универсально в React, Angular, Vue, Solid, Web Components и других компонентных библиотеках. Аналога в js нет
Страница - семантичный комплексный элемент, существующий с зарождения веба. По любому URL из браузера открывается страница (конечно, если не из браузера - то другая история и другие протоколы). Она может содержать компоненты или быть цельной независимой html-compatible структурой. Но компоненты не могут содержать страницу в прямом смысле, только через iframe, то есть отдельный URL. Аналога в js нет
Сторы - это либо Data store, который содержит в себе только данные, либо Model/Service/UI/ViewModel Store, которые могут содержать методы. В классике выбирают наименование, которое им более привычно, но все равно эти 2 типа как правило четко разделяются и присутствуют. Аналога в js нет
Действия - это имеющие доступ сторам и апи функции. Они могут быть частями Model/Service/UI Store, а могут быть отдельными. В ряде случаев их могут называть "контроллерами", они могут читать, писать, вызывать - делать все, что нужно для функционала. Аналога в js нет (ну кроме слова "функция", но оно очень общее)
Слои - это семантическое группирование по функционалу и свойствам. API-слой отвечает за получение данных из внешних источников, Reaction-слой (это могут быть хуки, реакции СТМ, эвенты) - за реагирование на сигналы, подаваемые компонентами либо страницами. Слой сторов - объяснил выше. Слой констант, слой утилит + в зависимости от архитектуры проекта. Слои могут быть глобальными (для всего приложения или для отдельного приложения мультирепы), могут быть постраничными (в ряде интерпретаций - модульными) и могут быть локальными. В классике всегда соблюдается принцип "низшие знают о высших, но высшие не знают о низших". Нереально редкие исключения, о которых я даже не могу предположить, чтобы что-то глобальное знало о локальном, решаются через события: LocalUiStore -> call event ('someName', payload) -> GlobalReactionStore -> get event ('someName', payload) -> setData to GlobalStore. Но в классике такого не бывает, чисто теория
Если в контексте вашего ответа, то "бизнес-сущность продукт" - это компонент src/components/Product. "следующие сущности: "карточка маленькая", "карточка большая", "список продуктов" решаются либо пропом к компоненту, либо маппером, когда в src/components/Product лежат ProductSmall.tsx, ProductBig.tsx, ProductList.tsx, хотя никто бы не стал список продуктов помещать в один компонент.
Авторизация в классике - это не "фича" в контекстуальном понимании FSD, это действие. Оно вызывает апи с параметрами и либо выдает нотификацию, либо кладет в стор юзера. Этот экшен могут вызывать как компоненты, так и страницы. В ряде архитектур могут и Model/Service/UI Store. "оплата", или "добавление в корзину", или "удаление из корзины" - то же самое. И в действии могут быть "апи и, например, оптимистичная мутация данных на фронте".
В целом классика максимально всеобъемлюща, уникальна для структуры папок, не пересекается с js по неймингу. Но она требует, как и FSD, определенного опыта для организации. Но документации для нее по факту нет, поэтому FSD в этом плане выигрывает. Но в большинстве случаев документация и не нужна, так как фронтендеры и так мыслят этими сущностями, и классика есть отражение логичного мышления.
Скажу сразу, с FSD в проде я работал только единожды (в остальном только с классической структурой), все что здесь напишу - мысленные эксперименты + небольшой опыт.
"Изолированность и модульность", включающая в себя удобства в виде "независимая от других разработка модуля одним разработчиком", "изоляция кода и багов", "масштабируемость".
Это звучит безусловно непротиворечиво - каждый пишет код в своей папке, возможно со своим стилем кода, несет за него ответственность и это вдохновляет писать качественно. Однако налицо и катастрофический недостаток в виде дубляжа. 5 разработчиков на своих 5 страницах делают одно и то же обращение к апи, модели запроса-ответа, реализацию обработки ошибок со своими текстами (условно - понадобилось стянуть справочник Cities с бэка). Также они пишут свои кнопки, свои формы, свою локализацию, свои интерфейсы к компонентам.
В ряде случаев все это будет уникальным для страницы / виджета / фичи, а в ряде случаев (предположу что 30%) это будет дубляжом. Соответственно если апи бэка изменился для этой ручки - то нужно менять в 5 модулях, если кнопку нужно перестилить или добавить состояние загрузки - тоже в 5 модулях нужно искать, разбираться в каждой реализации и делать уникальные решения для каждой уникальной, но похожей, кнопки.
В итоге на этапе разработки ресурс времени разработчиков экономится, однако в перспективе поддержки из-за неединообразия в реализации, но единообразия в функционале, выходит кратное возрастание затрат времени.
Класть компоненты в shared/ui для переиспользуемости - безусловно частично выправляет ситуацию. Однако с этим теряется и "модульность" - при разработке опять же 5 разрабам нужно добавить разный функционал в кнопку (иногда stopPropagation, иногда внезапно появившийся новый вариант цвета, иногда дополнительную анимацию). Они начинают "толкаться" в одном компоненте и в определенных проектах (например с релизным циклом в 1 день) решают ее скопировать в свой модуль, дополнительно увеличивая дубляж, так как решать все потенциальные мердж-конфликты заранее с другими разрабами и их локальными ветками через чат - это пытка.
Потом, разумеется, если разработка активная, факт дубляжа забывается, а через год разраб, делающий редизайн, видит десятки похожих кнопок с разной реализацией. Это история из жизни - как раз делал редизайн в таком проекте, который из-за начального решения следовать принципу изоляции начал реализовывать это через дубляж.
У вас идеально скомплектованная команда и хорошие процессы - перед стартом фронт-задачи есть ТЗ, дизайн, готовый бэк и время на анализ каждой задачи с командой + достаточное время на реализацию + есть время писать техническую документацию и обосновывать принятые решения + время на декомпозицию и слежение за статусами подзадач. К сожалению, таких компаний не так много, и часто есть только грубое ТЗ и примерный дизайн, и за 3 дня нужно сделать одновременно и параллельно дизайн + фронт + бэк и корректно интегрироваться, да и в процессе работы над задачей требования меняются вплоть до последнего часа. При этом бизнес очень жестко настаивает, что "там дел на 5 минут".
Это часто встречается в стартапах, которым за пару месяцев нужно сделал свой Гугл (а им всем нужно) и сделать это лучше, чем у конкурентов. Здесь на мой взгляд выиграет FSD за счет изоляции, если разрабов 3+, и проиграет классике, если меньше - классика заточена под переиспользуемость и глобальность + четкую структуру слоев, и из-за динамичных изменений и быстрой разработки проще поправить в 1 месте для всех 10 страниц, чем в каждом из них, так как нет достаточных ресурсов разработчиков.
Также вы говорили, что уделите основное внимание "процессу разработки на FSD", но большая часть статьи - про Agile (управление задачами, ретроспектива, распределение задач, обсуждение с коллегами, документирование, разбиение на итерации) - это универсальные принципы, никак не зависящие от структуры проекта, и те же самые этапы по Agile должны быть везде. В том числе в классике было бы то же самое (только с меньшей детализацией) при наличии таких же ресурсов и команды, только вместо обсуждений "куда положить то или иное и как назвать" больше бы обсуждались детали реализации (дебаунсы, стоит ли вынести функционал на слой выше для переиспользуемости, обсуждение с бэком удобного контракта апи).
В итогах вы тоже сравниваете сами с собой - насколько команда привыкла к структуре, насколько вначале было неэффективное разбиение задач, как вы постепенно улучшали структуру проекта и за счет "притирки" (давали бизнесу прежние сроки, а делали за меньшее время) высвободили время для рефакторинга и документации. Которая, к слову, в классике пишется очень просто, хотя ее по факту никто не читает - классика по большей части самодокументируется семантикой и слоями без искусственного разделения.
То есть статья получилась не про FSD, а про то, как хорошо когда в компании много ресурсов и времени, и можно пробовать что угодно, и все будет хорошо)
P.S. я делал похожий проект, только в соло и намного сложней (так как нужно было еще 3 проекта параллельно делать - приложения для сборщиков и курьеров, для мониторинга сотрудников, клиентский сайт) - на классическую структуру это все прекрасно ложится за счет ее структурированности и переиспользуемости, и все, что увидел в статье, никак не сподвигло бы на переход. Если нужно, могу рассказать о структуре этих страниц, но это вряд ли нужно - в классике и так все разбираются, поэтому и статей по ней нет.
Конечно, Button может получать данные наиболее удобным способом - через глобальный контекст, через прямой импорт глобального стора, через пропсы. Когда может быть удобнее использовать глобальный стор, а не пропсы?
Например, кнопка читает глобальную тему стилей и нужно на лету менять цвет всех кнопок на проекте. Допустим, что css variables не используются, а тема хранится в глобальном сторе или передается как во многих UI библиотеках через верхнеуровневый контекст, либо css in js. В этом случае безусловно через пропсы каждой кнопке передавать текущий цвет нелогично.
Второй пример - локализация. Есть 10 языков, данные текстов хранятся в глобальном сторе. Button сделали с таким интерфейсом, что текст опционален - `<Button text={store.localize('Ok')} />`, а по дефолту кнопка имеет текст Ok. Это часто используется например в модалках или конфермах, когда есть 2 кнопки - отказ и принять, и не хочется каждый раз тексты передавать через пропы. Разумеется, кнопка возьмет дефолтный текст из глобального стора, если хранение локализации находится там.
Третий пример - есть специфичная кнопка src/components/ButtonGetUser, которая используется на 5 страницах и должна вызвать апи получения данных по пользователю, и вызвать модальное окно с показом его данных. Этот кейс ближе к админкам. Зачем дублировать логику, передавая в onClick одну и ту же логику, если этот компонент может сам обратиться к глобальному апи и глобально вызвать модалку - DRY принцип в действии.
Я не задумываюсь о разделении "глупый компонент", "тупой компонент", "компонент, принимающий все только через пропы", "компонент, способный сам вызывать глобальные действия" - если того требует логика, компонент может трансформироваться между этими состояниями без лишней когнитивной нагрузки и выделения дополнительных сущностей - адаптеров, коннекторов, контроллеров, HOC, виджетов и что только не видел еще для "соблюдения неких принципов".
Про декорирование не понял, я вроде о нем ничего не говорил)