Интеграция двух инструментов только усложнила бы финальное решение, просто потому что появилось бы больше мест для потенциальной ошибки при поддержке этого всего. В то же время оказалось, что в нашем случае чаще всего нужно просто загрузить данные и отобразить их. То есть гибкость MobX оказывалась избыточной. Поэтому меньшим из зол в нашем случае оказалось перейти на React Query. Пусть и появляется бОльшая связь с представлением, флоу запросов получается более очевидным и не требующим такой степени абстракции. Если действительно понадобится сильнее разделить запрос (и возможную обработку) данных и отображение, будем думать, спасибо за идею. Хотя пока в большинстве случаев в нашем кейсе это всё-таки оказывалось лишним по крайней мере для наших целей
Да, я понял ваш довод про слои архитектуры. Я больше о том, что не обязательно одному и тому же стору знать о деталях взаимодействия с API (там же и кеширование, состояние пагинации и другие) и одновременно контролировать UI. У нас для взаимодействия с API были утилитарные сторы, которые брали на себя рутину (а-ля ApiStore, ApiRequestModel, PaginationModel). А сторы страниц использовали утилитарные сторы и были сосредоточены на отображении. Другое дело, что, действительно, все эти конструкции были избыточны в нашем случае
Да, раньше было ок то, что данные обнулялись. Просто в какой-то момент главная стала жирной, да и объективно её незачем было запрашивать при каждом перезаходе
В целом всё так, как вы предлагаете: можно было превратить в глобальный стор. Просто это же всю обвязку самим придётся реализовывать: у нас там фильтрация сущностей — их надо учитывать, stale-while-revalidate, политики инвалидации (staleTime, cacheTime, ручная инвалидация по ключам с полным или частичным совпадением). Пагинация даёт больше сложностей, тот же SSR
Это всё можно сделать и без React Query, конечно. В частном порядке для одной лишь какой-то страницы — тем более. Но при масштабировании это всё превратится в снежный ком, который заставит совершить много ошибок и исправить их. Вместо этого мы предпочли сделать проще и так, чтобы лучше подходило под наши нужды
Мне кажется, что ни один разработчик, сколько бы он ни был компентым, не сможет написать класс/функцию один раз и пользоваться дальше на протяжении всей жизни проекта. В начале не нужен кеш — потом стал нужен. Сначала не нужны ретраи — потом стали нужны. В нашем случае проект развивается уже больше полутора лет
У нас было два пути: 1) Продолжать реализовывать свои решения, теряя ресурсы (деньги, время) на отладку и доведение до идеала (не сомневаюсь, спустя N итераций мы бы всё же дошли до идеала); 2) Использовать готовое, проверенное сообществом решение
"Взять и отказаться от MobX просто так" не произошло так легко. Безусловно, рассматривались различные варианты. И в нашем случае лучше сыграл тот, который описал в статье. Это совершенно не значит, что MobX плох, или ранее избранный путь развития был плох сам по себе. Это значит, что для наших конкретных нужд старый подход себя изжил
Почему вам кажется, что стор должен ограничиваться только контролированием состояния UI? При желании очень хорошо можно выстроить взаимодействие с API и через MobX-сторы: глобальный стор для контроля за всеми запросами, общей конфигурацией; свой observable-экземпляр под каждый запрос со своим внутренним состоянием выполнения, однообразная отмена запросов и так далее. Опять же — вопрос в том, какой ценой команда готова это реализовывать и сколько шишек набьёт по пути
Статья сравнивает подходы. В одном подходе использовалась плотная связка стейт-менеджера и походов в API, в другом — фактически отказ от стейт-менеджера. Второй подход оказался нам ближе. Если бы были опущены конкретные библиотеки и детали, статья бы не несла пользы. Тогда бы было сравнение абстрактного подхода с другим абстрактным подходом. А так читатель хотя бы может увидеть конкретные примеры
Спасибо за очень развернутый комментарий и примеры! Мне тоже нравится MobX с классовым подходом тем, что можно чётко отделять логику и отображение
Вы правильно заметили, что подход с MobX включал в себя самостоятельное выстраивание флоу по работе с API и этот подход нам не подошёл. Мне хотелось подчеркнуть, что React Query решает множество проблем, которые мы пытались решить сами и что использование готовой библиотеки может помочь избежать риска совершить ещё больше ошибок. Да, всё можно выстроить грамотно и к способности это делать, безусловно, нужно стремиться. Просто в нашем случае было дешевле сменить подход
На самом деле, я пока писал статью, жёстко загорелся идеей написать сколько-то объёмные два приложения с одинаковым функционалом очень похожим на наш кейс, но чтобы общение с API было бы реализовано по-разному: через MobX и утилитарные классы и React Query. Это я хотел размеры бандлов сравнить, но чтобы не просто размер библиотек сравнивать, а чтобы tree shaking какой-нибудь сработал и другие оптимизации. И сделал это с помощью Codex от OpenAI за пару вечеров. Выглядело ужасно, но для эксперимента было достаточно. В итоге я не увидел значительного различия совсем. Почти одинаковые цифры по размерам бандлов. А что касается нашего кейса, поскольку две библиотеки пока живут вместе в одном проекте, не получится сказать о каком-то улучшении в этом плане
Если говорить о перерендерах, то, как уже отметил в статье, MobX позволяет точечно реагировать компонентам на изменение конкретных полей, которые они слушают. В React Query всё-таки когда изменяем кеш вручную, это делается иммутабельно, поэтому там могут быть лишние ререндеры. Но здесь уже вопрос о том, приемлемо ли это для конкретного приложения. Для нас это было ок
Касательно перформанса в смысле "абсолютной" скорости, а не "воспринимаемой", в плане общения с API фронтовая библиотека вряд ли что-то может предложить для улучшения ситуации с сетью, поэтому мне показалось более резонным сделать упор на воспринимаемую скорость и избавление от лишних запросов. Так, для среднего сеанса пользователя (типа "зашли на главную, подгрузили список, оттуда — в деталку, потом обратно"), я бы сказал, что мы избавились от лишних перезапросов почти полностью, поскольку данные не обновляются очень часто. В случае нашего предыдущего сетапа без кеширования у нас бы на каждый шаг происходил новый запрос
Спасибо за комментарий. Надеюсь, я донёс, почему рассматривал кейс по большей мере с точки зрения воспринимаемой скорости
Спасибо, что поделились опытом и подводными камнями!
Невозможность иметь несколько представлений для одних и тех же данных. Например мы отрисовали 2 списка в разных компонентах через хук и теперь нужно отсортировать список
У нас проект, честно сказать, не прям очень интерактивный и всё, что сложнее сортировки массива, мы просто перемещаем на сторону API. В тому же, скорее всего, такой список у нас был бы пагинируемым, а на клиенте пагинируемый список не отсортируешь, поэтому точно эти манипуляции мы бы оставили бэкенду
Ну и, вероятно, у вас чуть другой кейс. Для нас кейс с разными представлениями данных выглядел, скорее, так: юзер зашёл на главную — увидел там какой-то набор сущностей с минимальным количеством данных; зашёл в деталку какой-то сущности — там расширенный набор данных (данные из списка переиспользовать не получится); зашёл в поиск — там тоже некоторый список сущностей с разным количеством данных и не факт, что этот список пересекается с другими. В общем, для нас вопрос представления одних и тех же данных разным образом не стоял остро, но я согласен с вами, с React Query это, действительно, может быть неудобно
react-query это всё-таки просто кеш. Он не даёт вообще никаких удобств для написания интерактивных приложений
Полностью согласен с вами (и об этом упомянул в статье). В нашем случае не нужно было много интерактивности, поэтому React Query отлично подошёл. Максимум интерактивности в нашем кейсе — это добавление каких-то сущностей в избранное, например. Точно не дашборды или игровые механики
Спасибо за идею, действительно интересно бы было сделать такую либу. Но, боюсь, нам бы это дало не меньше головной боли с ревалидицией кеша (в смысле механизм stale-while-revalidate), его сбросом, его редактированием, отключением синхронизации на сервере и включением на клиенте. Всё так же надо было реализовывать вручную гидрацию стора на клиенте по данным с сервера. В общем, всё это действительно добавляет лишнего оверхеда, когда в React Query это уже всё реализовано и поддержано сообществом
Да, безусловно, проблема не в том, что MobX какой-то не такой, и я указал в статье, что просто дело в том, что React Query больше подошёл под наши потребности, а для своих нужд MobX — отличный инструмент
Мне кажется, даже если бы мы написали адаптер — это был бы больше костыль и лишнее усложнение в поддерживаемости кода. Мы бы в и без того сложную структуру встроили кеширование через React Query, а вместе с тем потянулись бы и сложности в SSR и другие. Вместо этого легче было написать запрос и завернуть его в useQuery/useInfiniteQuery, а все состояния загрузок, гидрацию, ревалидацию React Query решет под капотом. Для связи MobX и React Query нам достаточно было реагирования useEffect'ами без дополнительных обёрток. Вероятно, для проектов, где реактивность нужна более тонкая, адаптер бы подошёл лучше
По поводу пакета react-query-mobx, прошу прощения, мб не так искал — не нашёл. Нашёл только mobx-ecosystem/mobx-react-query с пятью скачиваниями за последнюю неделю. Кажется, это не то, что вы имели в виду
Про наследование да, нужно не заиграться в ООП во фронтенде хехе....
Сделать так, чтоб при уходе со страницы стор не очищался
Да, конечно, можно. Как уже говорил, всё, что есть в React Query, можно сделать вместе с MobX. По идее в том числе предложенным вами пакетом, просто вместо localStorage подсунуть какой-то глобальный storage (хранение в localStorage нам было бы избыточно). В целом можно бы было ещё проще — сделать локальный стор подобием глобального и сделать свои механизмы работы с таким подобием кеша
Но снова: пришлось бы явно разделять, как стор ведёт себя на клиенте и на сервере (на клиенте сохраняем состояние, на сервере всегда создаём новое); пришлось бы задумываться, как обрабатывать состояния загрузки при наличии persistent-состония; как реализовать stale-while-revalidate, а не просто сбрасывать состояние и загружать новое; как работать с пагинируемыми списками и так далее — всё это уже реализовано в React Query
Здесь же немаловажен вопрос поддерживаемости. Поскольку самая частая цель сторов на проекте была в том, чтобы "загрузить данные из API и хранить", переписывание на другую технологию заключалось просто в том, чтобы удалить стор, написать функцию запроса и обернуть её в useQuery/useInfiniteQuery, подобрав query-ключи — это буквально всё. Остальную рутину решает React Query под капотом, в том числе гидрацию при с SSR. На те грабли, которые я привёл в статье, мы бы в любом случае наступили, реализовывали бы мы своё решение или использовали бы что-то новое. Поэтому да, скорее, легче было просто переписать
Квери-параметры в смысле и размер страницы в квери-ключ положить? Так тоже можно, да. Получатся два разных ключа `{ page_size: 10 }` и `{ page_size: 20 }`. В целом на проекте так и делаем. Про строки `-fixed-size`, `-paginated` я для примера явного разделения разных ключей привёл пример. А так да, и параметры туда можно развернуть, спасибо за замечание
Наверное, нормальный человек просто не скажет такую фразу. Такая фраза не может звучать не как угроза. Мало ли чего от такого соседа можно ожидать, раз он такой мелочью угрожает (учитывая все другие его странности, рассказанные в статье). Ладно бы это друг сделал: он бы просто сфоткал и выложил шутки ради. А тут сосед странный какой-то
Так необычно осознавать, что 2010 год был 14 лет назад... Хотя да, будто отголоски чего-то тусовочного от старших курсов слышал, да и по квартире, в которой жил, можно было понять, что в недалёком прошлом веселье это помещение стороной не обошло, а вот среди своего потока не замечал особо. Может быть, просто повезло так, а может быть, тенденция
На самом деле, в московском политехе ни разу не сталкивался с типичной ~общажной~ атмосферой с песнями, алкоголем и драками. Не исключаю, что просто мне так везло или у меня получалось формировать свой пузырь-круг-общения, но и по этом своему окружению вижу, что часто студенты больше думают в сторону карьеры, особенно на it-направлениях. У меня знакомый уже на первом курсе на 40 часов в неделю вышел (до сих пор удивляюсь, как...) и ему общажные приколы не очень интересны были, очевидно. А насчёт раздражающих мелочей — да, это действительно надоедает за два года (в моём случае два), учитывая что, например, мне приходилось много работать и учиться именно в общаге, потому что офис и коворкинги были далеко. Да и, помимо мелочей, озвучены и более жёсткие штуки, поэтому в сумме такой эффект получается... И уже становится непонятно: это я неуравновешенный или с соседями что-то не так...
Интеграция двух инструментов только усложнила бы финальное решение, просто потому что появилось бы больше мест для потенциальной ошибки при поддержке этого всего. В то же время оказалось, что в нашем случае чаще всего нужно просто загрузить данные и отобразить их. То есть гибкость MobX оказывалась избыточной. Поэтому меньшим из зол в нашем случае оказалось перейти на React Query. Пусть и появляется бОльшая связь с представлением, флоу запросов получается более очевидным и не требующим такой степени абстракции. Если действительно понадобится сильнее разделить запрос (и возможную обработку) данных и отображение, будем думать, спасибо за идею. Хотя пока в большинстве случаев в нашем кейсе это всё-таки оказывалось лишним по крайней мере для наших целей
Да, я понял ваш довод про слои архитектуры. Я больше о том, что не обязательно одному и тому же стору знать о деталях взаимодействия с API (там же и кеширование, состояние пагинации и другие) и одновременно контролировать UI. У нас для взаимодействия с API были утилитарные сторы, которые брали на себя рутину (а-ля ApiStore, ApiRequestModel, PaginationModel). А сторы страниц использовали утилитарные сторы и были сосредоточены на отображении. Другое дело, что, действительно, все эти конструкции были избыточны в нашем случае
Да, раньше было ок то, что данные обнулялись. Просто в какой-то момент главная стала жирной, да и объективно её незачем было запрашивать при каждом перезаходе
В целом всё так, как вы предлагаете: можно было превратить в глобальный стор. Просто это же всю обвязку самим придётся реализовывать: у нас там фильтрация сущностей — их надо учитывать, stale-while-revalidate, политики инвалидации (staleTime, cacheTime, ручная инвалидация по ключам с полным или частичным совпадением). Пагинация даёт больше сложностей, тот же SSR
Это всё можно сделать и без React Query, конечно. В частном порядке для одной лишь какой-то страницы — тем более. Но при масштабировании это всё превратится в снежный ком, который заставит совершить много ошибок и исправить их. Вместо этого мы предпочли сделать проще и так, чтобы лучше подходило под наши нужды
Мне кажется, что ни один разработчик, сколько бы он ни был компентым, не сможет написать класс/функцию один раз и пользоваться дальше на протяжении всей жизни проекта. В начале не нужен кеш — потом стал нужен. Сначала не нужны ретраи — потом стали нужны. В нашем случае проект развивается уже больше полутора лет
У нас было два пути: 1) Продолжать реализовывать свои решения, теряя ресурсы (деньги, время) на отладку и доведение до идеала (не сомневаюсь, спустя N итераций мы бы всё же дошли до идеала); 2) Использовать готовое, проверенное сообществом решение
"Взять и отказаться от MobX просто так" не произошло так легко. Безусловно, рассматривались различные варианты. И в нашем случае лучше сыграл тот, который описал в статье. Это совершенно не значит, что MobX плох, или ранее избранный путь развития был плох сам по себе. Это значит, что для наших конкретных нужд старый подход себя изжил
Почему вам кажется, что стор должен ограничиваться только контролированием состояния UI? При желании очень хорошо можно выстроить взаимодействие с API и через MobX-сторы: глобальный стор для контроля за всеми запросами, общей конфигурацией; свой observable-экземпляр под каждый запрос со своим внутренним состоянием выполнения, однообразная отмена запросов и так далее. Опять же — вопрос в том, какой ценой команда готова это реализовывать и сколько шишек набьёт по пути
Статья сравнивает подходы. В одном подходе использовалась плотная связка стейт-менеджера и походов в API, в другом — фактически отказ от стейт-менеджера. Второй подход оказался нам ближе. Если бы были опущены конкретные библиотеки и детали, статья бы не несла пользы. Тогда бы было сравнение абстрактного подхода с другим абстрактным подходом. А так читатель хотя бы может увидеть конкретные примеры
Спасибо за очень развернутый комментарий и примеры! Мне тоже нравится MobX с классовым подходом тем, что можно чётко отделять логику и отображение
Вы правильно заметили, что подход с MobX включал в себя самостоятельное выстраивание флоу по работе с API и этот подход нам не подошёл. Мне хотелось подчеркнуть, что React Query решает множество проблем, которые мы пытались решить сами и что использование готовой библиотеки может помочь избежать риска совершить ещё больше ошибок. Да, всё можно выстроить грамотно и к способности это делать, безусловно, нужно стремиться. Просто в нашем случае было дешевле сменить подход
На самом деле, я пока писал статью, жёстко загорелся идеей написать сколько-то объёмные два приложения с одинаковым функционалом очень похожим на наш кейс, но чтобы общение с API было бы реализовано по-разному: через MobX и утилитарные классы и React Query. Это я хотел размеры бандлов сравнить, но чтобы не просто размер библиотек сравнивать, а чтобы tree shaking какой-нибудь сработал и другие оптимизации. И сделал это с помощью Codex от OpenAI за пару вечеров. Выглядело ужасно, но для эксперимента было достаточно. В итоге я не увидел значительного различия совсем. Почти одинаковые цифры по размерам бандлов. А что касается нашего кейса, поскольку две библиотеки пока живут вместе в одном проекте, не получится сказать о каком-то улучшении в этом плане
Если говорить о перерендерах, то, как уже отметил в статье, MobX позволяет точечно реагировать компонентам на изменение конкретных полей, которые они слушают. В React Query всё-таки когда изменяем кеш вручную, это делается иммутабельно, поэтому там могут быть лишние ререндеры. Но здесь уже вопрос о том, приемлемо ли это для конкретного приложения. Для нас это было ок
Касательно перформанса в смысле "абсолютной" скорости, а не "воспринимаемой", в плане общения с API фронтовая библиотека вряд ли что-то может предложить для улучшения ситуации с сетью, поэтому мне показалось более резонным сделать упор на воспринимаемую скорость и избавление от лишних запросов. Так, для среднего сеанса пользователя (типа "зашли на главную, подгрузили список, оттуда — в деталку, потом обратно"), я бы сказал, что мы избавились от лишних перезапросов почти полностью, поскольку данные не обновляются очень часто. В случае нашего предыдущего сетапа без кеширования у нас бы на каждый шаг происходил новый запрос
Спасибо за комментарий. Надеюсь, я донёс, почему рассматривал кейс по большей мере с точки зрения воспринимаемой скорости
О, выглядит интересно, спасибо
Спасибо, что поделились опытом и подводными камнями!
У нас проект, честно сказать, не прям очень интерактивный и всё, что сложнее сортировки массива, мы просто перемещаем на сторону API. В тому же, скорее всего, такой список у нас был бы пагинируемым, а на клиенте пагинируемый список не отсортируешь, поэтому точно эти манипуляции мы бы оставили бэкенду
Ну и, вероятно, у вас чуть другой кейс. Для нас кейс с разными представлениями данных выглядел, скорее, так: юзер зашёл на главную — увидел там какой-то набор сущностей с минимальным количеством данных; зашёл в деталку какой-то сущности — там расширенный набор данных (данные из списка переиспользовать не получится); зашёл в поиск — там тоже некоторый список сущностей с разным количеством данных и не факт, что этот список пересекается с другими. В общем, для нас вопрос представления одних и тех же данных разным образом не стоял остро, но я согласен с вами, с React Query это, действительно, может быть неудобно
Полностью согласен с вами (и об этом упомянул в статье). В нашем случае не нужно было много интерактивности, поэтому React Query отлично подошёл. Максимум интерактивности в нашем кейсе — это добавление каких-то сущностей в избранное, например. Точно не дашборды или игровые механики
Спасибо за идею, действительно интересно бы было сделать такую либу. Но, боюсь, нам бы это дало не меньше головной боли с ревалидицией кеша (в смысле механизм stale-while-revalidate), его сбросом, его редактированием, отключением синхронизации на сервере и включением на клиенте. Всё так же надо было реализовывать вручную гидрацию стора на клиенте по данным с сервера. В общем, всё это действительно добавляет лишнего оверхеда, когда в React Query это уже всё реализовано и поддержано сообществом
Да, безусловно, проблема не в том, что MobX какой-то не такой, и я указал в статье, что просто дело в том, что React Query больше подошёл под наши потребности, а для своих нужд MobX — отличный инструмент
Мне кажется, даже если бы мы написали адаптер — это был бы больше костыль и лишнее усложнение в поддерживаемости кода. Мы бы в и без того сложную структуру встроили кеширование через React Query, а вместе с тем потянулись бы и сложности в SSR и другие. Вместо этого легче было написать запрос и завернуть его в useQuery/useInfiniteQuery, а все состояния загрузок, гидрацию, ревалидацию React Query решет под капотом. Для связи MobX и React Query нам достаточно было реагирования useEffect'ами без дополнительных обёрток. Вероятно, для проектов, где реактивность нужна более тонкая, адаптер бы подошёл лучше
По поводу пакета react-query-mobx, прошу прощения, мб не так искал — не нашёл. Нашёл только mobx-ecosystem/mobx-react-query с пятью скачиваниями за последнюю неделю. Кажется, это не то, что вы имели в виду
Про наследование да, нужно не заиграться в ООП во фронтенде хехе....
А как с Vue+Pinia? Там это проще решается? Я сам никогда Vue плотно не изучал, интересно, как там
Да, конечно, можно. Как уже говорил, всё, что есть в React Query, можно сделать вместе с MobX. По идее в том числе предложенным вами пакетом, просто вместо localStorage подсунуть какой-то глобальный storage (хранение в localStorage нам было бы избыточно). В целом можно бы было ещё проще — сделать локальный стор подобием глобального и сделать свои механизмы работы с таким подобием кеша
Но снова: пришлось бы явно разделять, как стор ведёт себя на клиенте и на сервере (на клиенте сохраняем состояние, на сервере всегда создаём новое); пришлось бы задумываться, как обрабатывать состояния загрузки при наличии persistent-состония; как реализовать stale-while-revalidate, а не просто сбрасывать состояние и загружать новое; как работать с пагинируемыми списками и так далее — всё это уже реализовано в React Query
Здесь же немаловажен вопрос поддерживаемости. Поскольку самая частая цель сторов на проекте была в том, чтобы "загрузить данные из API и хранить", переписывание на другую технологию заключалось просто в том, чтобы удалить стор, написать функцию запроса и обернуть её в useQuery/useInfiniteQuery, подобрав query-ключи — это буквально всё. Остальную рутину решает React Query под капотом, в том числе гидрацию при с SSR. На те грабли, которые я привёл в статье, мы бы в любом случае наступили, реализовывали бы мы своё решение или использовали бы что-то новое. Поэтому да, скорее, легче было просто переписать
Квери-параметры в смысле и размер страницы в квери-ключ положить? Так тоже можно, да. Получатся два разных ключа `{ page_size: 10 }` и `{ page_size: 20 }`. В целом на проекте так и делаем. Про строки `-fixed-size`, `-paginated` я для примера явного разделения разных ключей привёл пример. А так да, и параметры туда можно развернуть, спасибо за замечание
Наверное, нормальный человек просто не скажет такую фразу. Такая фраза не может звучать не как угроза. Мало ли чего от такого соседа можно ожидать, раз он такой мелочью угрожает (учитывая все другие его странности, рассказанные в статье). Ладно бы это друг сделал: он бы просто сфоткал и выложил шутки ради. А тут сосед странный какой-то
Так необычно осознавать, что 2010 год был 14 лет назад... Хотя да, будто отголоски чего-то тусовочного от старших курсов слышал, да и по квартире, в которой жил, можно было понять, что в недалёком прошлом веселье это помещение стороной не обошло, а вот среди своего потока не замечал особо. Может быть, просто повезло так, а может быть, тенденция
На самом деле, в московском политехе ни разу не сталкивался с типичной ~общажной~ атмосферой с песнями, алкоголем и драками. Не исключаю, что просто мне так везло или у меня получалось формировать свой пузырь-круг-общения, но и по этом своему окружению вижу, что часто студенты больше думают в сторону карьеры, особенно на it-направлениях. У меня знакомый уже на первом курсе на 40 часов в неделю вышел (до сих пор удивляюсь, как...) и ему общажные приколы не очень интересны были, очевидно. А насчёт раздражающих мелочей — да, это действительно надоедает за два года (в моём случае два), учитывая что, например, мне приходилось много работать и учиться именно в общаге, потому что офис и коворкинги были далеко. Да и, помимо мелочей, озвучены и более жёсткие штуки, поэтому в сумме такой эффект получается... И уже становится непонятно: это я неуравновешенный или с соседями что-то не так...