Дисклеймер
Уважаемый читатель! Если вы не имеете понятия, что такое React и Redux, читать дальше не имеет смысла, дальше технический бред. Я серьезно, понимание к чему эта записка, требует работы с указанными библиотеками — несмотря на то, что я постараюсь писать понятно, это статья не начального уровня. А еще это личный опыт и мнение, основанные на практике.
Что не так с использованием Redux?
Тут я надумал написать, а что собственно не так с использованием redux в моем проекте и тысячах других? Я же пишу приложения на react / redux эдак с апреля 2016 года(три года). Пора бы уже открыть некие интересности… А то лекции и доклады, особенно нацеленные на новичков, были, а вот какого-то взрослого взгляда назад и ретроспективы не было. А пока кто-то ставит звездочки пакетам, которые проверяют «а не 13 ли ты часом», я буду разбивать стену сложившихся стереотипов.
Но redux не нужен!
Скажете вы и в чем-то будете правы. Его можно не использовать с 2018го — в интернете тонна статей на эту тему, но пока с «неиспользованием» получается колхоз, ниже станет понятно почему. А уж количество альтернатив зашкаливает и подавно.
Мы используем Redux, потому что это принятый стандарт(во всяком случае для react), нам важна предсказуемость и надежность, которая есть у Redux. Но мы ее серьезно недополучаем, собственно в этом
Претензия
Наверное, чтобы было понятно, в чем претензия по пунктам, нужно сперва вернуться в прошлое, к истокам, как угодно, а именно открыть документацию славного redux и прочитать постулаты. Я рассмотрю только первый из них. Если вам будет интересно — делитесь в комментариях, разберу еще много моментов.
Единственный источник правды…
Вот здесь у нас засада. Конечно, говорится там о том, что сторов может быть 1, redux декларирует свое отличие от flux архитектуры таким образом. Но если смотреть шире: соблюдается ли правило в реальном проекте? Вы скажете: вполне. Я же декларирую(объявляю) 1 стор, его передаю в провайдер и тогда…
Технически процесс описываете верно. Но я могу сказать, что люди подвержены искажениям восприятия, логики, и, поскольку программисты все еще люди… Ну вы поняли, к чему я веду(если не поняли: программисты склонны к искажениям восприятия, логики и т. д.)
Главное искажение тут в том, что я, как и многие мои коллеги, привыкли, что если мы не юзаем термин «store» по отношению ни к чему, кроме redux, то и store других нет.
И тут приходит реакт
Одна техническая особенность, которая зовется internal state, плевать хотела на этот постулат. Она просто создает стор типа internal state в любом удобном месте. Есть компонент — есть стейт, который имеет механизм обновления и влияния на компонент. Отличие в целях использования (хранить стейт, обновлять и транслировать изменения) — почти незаметны. Вы можете возразить: не вполне понятно, чем тебе не нравится стейт компонента. Он же не такой как редакс, как их вообще можно сравнивать! Он внутренний, ну там флаги хранить какие.
Поймите, суть не поменяется от того, что вы переименуете предмет. Есть кувалда понедельник, это не делает ее днем недели. Да, оба понедельника тяжелые, другие дни и кувалды полегче. Но от своего названия кувалда не перестает быть ударным инструментом.
Масштаб у redux и state react-компонента разный, но суть, при их сегодняшнем использовании, одна.
Объясню это так: данные из internal state компонента в подавляющем большинстве случаев попадают в дочерние через props, но, как бы очевидно это не звучало —данные redux при интеграции с react тоже попадают в компоненты через props. С точки зрения компонента, который принимает props — неважно, что снаружи. То есть для конечного потребителя — что redux store, что internal state — одинаково.
Еще этот internal state может не зависеть от props того компонента, в котором он объявлен. Тогда получается изоляция, которая делает такой internal state еще большим store, чем можно представить.
Чтобы он был внутренним по-настоящему, он должен влиять только на компонент, где объявлен, не давая утечек в дочерние компоненты. Сложно? Весьма, ведь смысл его почти весь теряется. Это еще 1 признак того, что internal state — это store. Ведь мы просто убрали из назначения «хранить стейт, обновлять и транслировать изменения» один пункт — трансляцию изменений. Все, стейт потерян.
То есть основная проблема наличия internal state — он конкурирует с redux за данные, проигрывает в долгосрочной перспективе и гадит. У нас появляются всякие техники типа lift state up (это когда данные нужны сиблингу хост элемента, поэтому часть state и всю логику работы с ней выносят в родителя, тратя уйму времени на переписывание и тестирование рабочей логики) и т. д. Появляются баги, оверхед в доработках и много-много радости. Это все шум, который портит наше ПО еще на этапе разработки. Мы не докатились до прода, но ПО уже такое себе.
То есть по всем признакам и проблемам, у нас есть больше одного стора и много проблем с этим связанных. Финальной, наверно, вещью станет следующее:
Я очень люблю redux еще и за то, какой devtools у него есть. Когда я начинал мы использовали логгер, который просто консолил все экшены, не давая, впрочем, цельную картину происходящего. Теперь же — это основной помощник и друг. У React они тоже представлены. Вообще, devtools — причина по которой любому pubsub далеко до Redux. Как муравью до синего кита.
Проблемы(пруффов не будет — DNA):
- изменение internal state через react devtools иногда не приводит к обновлению или желаемому результату — грешу на интеграцию с redux.
- internal state ломает timetravel в redux devtools. Супер фича с путешестивием во времени, доступная благодаря архитектуре redux не работает благодаре архитектуре react internal state. Internal state просто плевал на изменение в redux, у него свое состояние. Таймтревел просто не выходит. Часть элементов просто не обновляется, частично обновляется и т. д. Вся эта эпопея с синхронизацией асинхронного кода коту под хвост.
Пример, конечно же, высосанный из практики
Вы работаете на новом для вас проекте, или ваш коллега писал некую функциональность год назад, а вам теперь ее нужно расширять. В общем, есть задача на доработку чужого кода. Вы начинаете расследовать, и понимаете, что в редакс данных нет. В коде нет экшенов, редьюсеров, что хранят нужные вам данные. И вы начинаете путешествие по дереву компонентов поисках заветного и находите их (!!!) даже несколько штук. Вы спрашиваете у коллег, но ответ стандартный: я не помню, в стейт писать быстрее, у нас не было времени и прочее. Вы идете в исходники и понимаете, что текущее его состояние не предполагает доработку. Вы переписываете рабочий оттестированный код, чтобы внести изменения и добавить новую функциональность.
Наличие пагубной альтернативы в виде internal state делает свое черное дело. Ведь сейчас это дешево, и не важно, что будет через год.
Немного метафор
Похоже на плохую еду — вроде вкусно и дешево, но через год или 3 — ЖКТ перестает подчиняться и живет своей жизнью. Вы тратите много времени и денег на возвращение былого здоровья и не всегда в этом преуспеваете.
Redux и React Internal State — конкуренты, как крупный и мелкий бизнес в одной нише. Основной товар — данные и влияние. ПО — потребитель их продукции. Можно привести много аналогий, но суть остается одна — когда мы разрабатываем ПО — нам конкуренция не нужна.
Мы «диктаторы» программного кода и любая конкуренция должна пресекаться, свободный рынок запрещаться, а плановая экономика и государственная монополия должна довлеть над потребителем.
Кхм, что-то меня понесло. Все должно идти по плану, в общем. У нас спринты, релизы и прочее, а у ПО конечная стоимость и время жизни/выхода на рынок. Это очень важно, и мы не можем позволить бунт на корабле, восстание библиотек.
Вывод простой
Не используйте вместе с redux других хранилищ. Исключения могут составить только совсем уж изолированные кейсы. Например, компоненты которые в принципе не управляются redux без соответствующей прослойки и не влияют на него.
Пример
Я разрабатывал standalone модуль в одной ветке и рефакторинг store в другой — вообще мой подход к управлению store и state отдельная тема для публикации. Я начал рефакторинг до модуля, но на момент как начала, так и окончания написания модуля, рефакторинг в тест и в мастер не ушел. Рефакторинг большой и требует вдумчивого регресса, который нужно запланировать — в общем, нельзя просто взять и отрефакторить.
Поэтому, зная о грядущих изменениях в store, я не стал использовать его для разработки новой фичи. Это увеличило бы затраты на актуализацию заброшенного рефакторинга и тестов в разы.
Что я сделал: подписался на минимум данных. Данные и их структура не страдали от рефакторинга, страдал код который их формировал, сохранял и т. д. В редакс не пишу ни байта. Проверяю залогинен ли пользователь и еще пару полей.
Для своих нужд я запилил PubSub с каналами и простым API. Да, да, пабсаб. Отсутствие нормальных девтулзов боль. Тайм-тревела — боль. В общем, я планирую написать расширение для хром в виде devtools и возможно опубликовать на гитхабе реимплементацию как конкурента redux. У меня есть тонна претензий к redux, которые я в этой статье поднимать не стану, но такой вот PubSub их практически не имеет. Кроме того, что я вспомнил redux logger…
И таким образом у модуля собственное хранилище, свое соединение с сервером.
То есть redux совсем не влияет на модуль, практически не может повлиять на это хранилище(всего три поля в подписке), но и модуль, и PubSub на redux никак не влияет. Такое разделение исключает конкруренцию.
Вопрос «а где хранить данные?» при разработке модуля у меня не возник ни разу. Но когда речь об redux vs internal state — у многих этот вопрос возникает практически постоянно. Я решил ответить на этот вопрос раз и навсегда.
Мое архитекторское мнение такое:
Храните данные только в redux(все-все, даже «внутренние»), если он подключен к вашему react-проекту в качестве основного хранилища, не используйте хранилищ которые будут конкурировать с ним. Это повысит надежность и отдачу от этой библиотеки и ее devtools(таймтревел и слежение за всеми данными по шагам ускоряют разработку и поиск возможных проблем — синхронные изменения дебажить круче и проще асинхронных).
Возможно стоит дописать библиотеку которая полностью исключает internal state из разработки? Или подменяет internal state на выборку из redux, например? Одну такую я начал писать еще год назад, закончил на 90 процентов, даже провел 1 доклад. Что скажете? Нужны такие?
Надеюсь, эта записка вам понравилась :)
Комментарии (78)
hazratgs
29.03.2019 19:55Так проблема в подходе или в людях? Я вот начал практиковать flutter, и могу сказать что redux я там с большой радостью использую, не представляю другой подход к разработке
kurt_live Автор
29.03.2019 21:30Однозначно сказать нельзя. Разработчик может быть великолепным специалистом и при этом не быть архитектором, а ПО — быть более универсальным, чем нужно проекту. В данном случае соединяя несколько библиотек в стек, разработчики не всегда оценивают плохие и хорошие стороны в полной мере. На волне хайпа зачастую могут пропустить некое несоответствие и даже через некоторое время не уяснить, почему мы делаем все по канону, а выходит не то, что ожидалось. В данном случае совместное использование internal state и redux — бомба с часовым механизмом.
EaGames
29.03.2019 21:17Все это конечно хорошо, но я бы небыл столько категоричен. Обновить стейт редакса стоит куда дороже чем локальный (который не совсем локальный но все-же).
kurt_live Автор
29.03.2019 21:23Поэтому я и привел аналогию про дешевую еду. В краткосрочной перспективе state выглядит проще. Однако, поскольку я не раз работал с долгосрочными проектами, затраты на поддержку и доработку изначально более "дешевого" подхода выливаются в десятки часов на простые в сущности вещи. Речь о проектах с более чем годом активной разработки.
vintage
30.03.2019 06:44А вы не думали, что проблема в самом реакте и его архитектуре?
kurt_live Автор
30.03.2019 17:59Несомненно в реакте, редаксе и прочих библиотеках куча аврхитектурных проблем. Из-за этих проблем иногда проще писать на чистом js. Но основная проблема в том, что нет библиотеки которая проблем лишена совсем. Чуть сложнее решение — все, есть проблемы. Другое дело знать, как писать так, чтобы проблем этих не получать. Чтобы "не стрелять в ногу"
vintage
30.03.2019 18:06-2Я вижу только одну проблему: между библиотекой, создающей много проблем, и создающей мало, люди с упорством достойного лучшего применения используют первую, аргументируя тем, что «нет идеала в этом мире».
kurt_live Автор
30.03.2019 20:33Вот только не попробуешь, не узнаешь, а попробовал — поддерживай. У вас есть серебрянная пуля? Поделитесь, я застрелюсь. А если серьезно, то есть бизнес, требования заказчиков, хайп и много чего, что влияет на выбор и поддержку. Посоветовал бы я редакс кому-то сейчас? Нет. Но и то, это "нет" со звездочкой. Почему? Потому что есть и под этот инструмент свои задачи. Вообще, нет ничего лучше, чем знать свой инструмент настолько, чтобы критика и реальность совпадали.
anfilat
29.03.2019 23:09-1Redux это явный антипаттерн, он создает слишком большую связанность. В среднем SPA несколько десятков страниц с разными данными и разной логикой работы с ними. И пытаясь что-то изменить в одном месте, приходится думать, как это повлияет на все остальное. А понять это крайне сложно, потому что каждую логическую операцию зачем-то разбивают на несколько частей и распихивают по разным файлам.
Никак не могу понять, почему этот бред стал таким популярнымhazratgs
30.03.2019 00:00Как объект с данными находящимся в определенном месте может создать бардак? Экшены отдельно, редьюсеры отдельно, саги отдельно. Если ты меняешь экшен то везде поменяется, я предоставить не могу с редаксом то что ты описал…
anfilat
30.03.2019 09:37Так в этом и проблема — «если ты меняешь экшен то везде поменяется»! Я делая один кусочек приложения, должен думать как это повлияет на все приложение. Которое кроме меня еще человек двадцать пишут и я даже не знаю, что там у них должно быть
hazratgs
30.03.2019 10:39Ну не знаю, по моей логике например кнопка купить должна везде работать одинакова на всех страницах.
kurt_live Автор
30.03.2019 18:13Ну это не антипаттерн, как мне кажется. Паттернов в нем несколько, соединенных в 1. Стор — это пабсаб с 1 каналом. ты всегда подписываешься на все данные в канале. Редакс просто не дифференцирует на что конкретно ты подписан. Он не следит за тем как ты меняешь стейт. Ему вообще все равно что происходит снаружи внутри. ссылка на корневую сущность поменялась — получите и распишитесь. 1 точка входа, одна выхода. это просто с точки зрения внесения изменений, последовательности, предсказуемости. Ну и все. это "state manager" а не "subscribe manager" или "changе manager". Я не выгораживаю тут редакс, а страдаю от этого. Мне не хватает каналов. Когда используешь каналы наглядно видишь разницу — редакс дергает подписчика раз в 40 чаще, обеспечивая 39 ложных срабатываний из 40. Помножить на количество коннектов и становится не по себе. Конечно они работают асинхронно и до поры до времени вы не заметите проблемы, но тем не менее...
bgnx
30.03.2019 02:17+2Мне кажется вы неправильно понимаете проблемы redux
Представьте сложное spa-приложение, какую-нибудь навороченную админку где юзер может одновременно открыть и расположить по экрану кучу панелей, окошек, cлайдеров, табов, всплывающих попапов с детализацией и т.д. которые будут отображать информацию об одних и тех же данных только с разными фильтрами, сортировкой, аггрегацией, детализацией и т.п. И теперь допустим в отдельной форме меняется какой-то один объект-сущность и нужно обновить все места которые отображают данные этого объекта. И появляется вопрос как определить какие компоненты отображают в текущий момент информацию об объекте который изменился чтобы выполнить перерендер (diff) только этих компонентов а не всего приложения?
С редаксом можно этого достичь только если законнектить каждый компонент и делать полностью плоский стейт где будет соответствие {[айдишник]: объект} а
все one-to-many и many-to-many связи моделировать через айдишники. Это нужно делать потому что независимо от того во скольких местах интерфейса отображается сущность — если данные о ней будут храниться только в одном месте то это избавляет от проблемы ручной синхронизации и необходимости применять изменение во многих местах. А ручная синхронизация еще хуже — вот статья на тему сложности синхронизации — https://hackernoon.com/data-denormalization-is-broken-7b697352f405 — там правда про базу данных на бекэнде но это соответствует модели редакса когда на одно событие может реагировать много редюсеров).
И в результате с редаксом даже с нормализованным стором получается ужасно неудобное решение потому что придется вручную джойнить данные во вьюхах (точнее в mapStateToProps) и обработчиках и любая бизнес-логика где необходимо обращаться к связанным сущностям будет сплошь и рядом пронизана вытаскиванием нужных объектов по их айдишникам.
А вот mobx позволяет не делать нормализацию а хранить объекты в их древовидно-графовом виде. То есть если например есть много таблиц связанных one-to-many или many-to-many то не нужно никаких айдишников — объекты можно прямо связывать ссылками друг на друга. Например если есть приложение где пользователь может создавать папки а внутри папок проекты а внутри проектов задачи и внутри них комментарии то эту схему можно один в один отобразить на объектах — будет объект пользователя в котором будет находиться массив папок ({firstName: "...", email: "", folders: [{..}, {...}, {...}]}), и каждая папка будет хранить объект проекта который будет хранить вложенный массив проектов ( {name: "folder1", projects: [{..}, {..}, {}]}, где каждый объект проекта будет хранить также массив ссылок на вложенные задачи а задача хранить вложенный массив комментариев и т.д, причем еще удобно делать обратные ссылки на родительские объекты чтобы можно было удобно обращаться через точку (например project.folder.priority) а не передавать через пропсы или контекст что требует изменений родительских компонентов.
И таким образом можно удобно работать с данными моделируя связи через ссылки (включая циклические связи между объектами когда вложенный объект имеет ссылку на родителя или в случае many-many связей когда например у проекта может быть много пользователей) и все это позволяет обращаться к нужным сущностям через точку без необходимости джойнить их по айдишникам как в redux.
Вот сравните как "удобно" обратиться к родительской папки из комментария (сквозь промежуточные объекты задачи и проекта) в редаксе когда мы делаем это через айдишники
state.folders[state.projects[state.tasks[comment.taskId].projectId].folderId].priority
А теперь как это проще и наглядней можно делать c mobx
comment.task.project.folder.priority
И mobx при этом еще и обеспечивает точечное обновление компонентов без каких-либо неудобств (достаточно только обернуть поля объектов декоратором observable а сами компоненты декоратором observer) и с большей эффективностью — в отличие от редакс ему не нужно делать цикл по всем подключенным компонентам (чтобы в mapStateToProps сравнить новый и старый объект и понять нужно ли выполнять обновление компонента) — mobx при изменении объекта сразу знает какие компоненты нужно обновить так как хранит список этих компонентов на нужном поле объекта. И если произойдет изменение одного объекта то будет выполнено обновление только тех реактовских компонентов которые в текущий момент отображают этот объект. А если будет происходить изменение сразу многих полей или если компонент зависит то от одних данных то от других данных (всякие условия в шаблоне) то mobx умеет это оптимизировать и выполнять обновление в одном проходе и только тех компонентов которые зависят от нужных изменений в конкретный текущий момент (то есть компонент не будет лишний раз обновляться если поменялись данные в неактивном бранче условия)
В итоге юзеру не нужно думать в каких местах интерфейса одновременно находятся одни и те же данные при их изменении (сколько панелей/окошке открыто) и уж тем более не синхронизировать состояние отдельных компонентов вручнуюkurt_live Автор
30.03.2019 20:24Наверное самый сложный ответ.
Мне кажется
Да, именно. Вам кажется. Ваш комментарий бьет по вполне понятной проблеме, что редакс плох в плане синхронизации данных, нормализации и прочего. Он отвратителен, несомненно. Тут я написал
https://habr.com/ru/post/445966/#comment_19963356 что оно такое. Проблема которую я осветил в статье лежит в другом месте, если так можно сказать. Я сказал о том, что есть конфликт редакса и реакта, который разрушает архитектуру приложения. При этом ни проблем непосредственно редакса, ни стейта компонента я не касался. Вывод из статьи просто говорит что использование с редаксом других стейт-менеджеров приведет к конфликту и проблемам.
Представьте сложное spa-приложение
Зачем представлять? Я SPA приложения пишу каждый день уже более 4х лет, все они сложные высоко-нагруженные и навороченные. Соцсети, админки, приложения с математикой, графикой и многим-многим другим.
И в результате с редаксом даже с нормализованным стором получается ужасно неудобное решение потому что придется вручную джойнить данные во вьюхах (точнее в mapStateToProps) и обработчиках и любая бизнес-логика где необходимо обращаться к связанным сущностям будет сплошь и рядом пронизана вытаскиванием нужных объектов по их айдишникам.
Странно ожидать от инструмента вещей, которых он не умеет. То есть вы изначально на компонент системы который служит некой оберткой для вашего кода, возлагаете надежды как на постгрес. Постгрес просто не даст вам определять многих вещей, заставит вас использовать типы, отдельный язык и наложит кучу ограничений. Да, взамен он даст много, но и потребует тоже. Еще и отдельный сервер в придачу. Тут вы сами описываете механизм обновления, используя тот же язык что и всегда, ни типов, ни обязательств. И хотите, чтоб оно работало, так же хорошо как бд? Мне кажется тут нужен другой инструмент, а не редакс. Графкьюэль его зовут...
А вот mobx позволяет не делать нормализацию
так и редакс не запрещает. поймите, вы от носка хотите, чтобы он был ракетой.
Вот сравните как "удобно" обратиться к родительской папки из комментария
Шах и мат, Дэн Абрамов. Извините меня за шутки, просто я нахожу это забавным. Вы можете сделать тоже самое и в редакс, просто там ход другой: простые объекты, иммутабельность… Но ссылки можно сделать и там, а еще есть Map, он позволяет связывать объекты. Да безусловно, не имея никакого нужного механизма в чулке под названием redux — можно говорить что это вина redux. Но поймите, он не варит кофе, не стирает трусы, но как чулок — он восхитителен. В любом случае, недостатки редакс я в статье не рассматривал. Понял свой промах, рассмотрю в другой раз. Статья же о clash of redux and react, который мало кто замечает, но который тем не менее влияет на итоговый продукт и на его разработку.
bgnx
31.03.2019 01:17Странно ожидать от инструмента вещей, которых он не умеет. То есть вы изначально на компонент системы который служит некой оберткой для вашего кода, возлагаете надежды как на постгрес.
так и редакс не запрещает. поймите, вы от носка хотите, чтобы он был ракетой.
Это не редакс не умеет, это он как раз таки и запрещает обычным js-объектам ссылаться друг на друга когда не используются никакие стейт-менеджеры. Почему запрещает? Потому что иммутабельный подход редакса говорит о том что нужно вернуть новый объект состояния. А если изменения произошли где-то глубоко в этом объекте то нужно также вернуть новый объект всех его предков. А теперь что будет если объекты будут ссылаться друг на друга? В простом примере когда юзер может создавать проекты и в проектах задачи и если каждая задача будет вместо айдишника (task.projectId) хранить ссылку на сам объект проекта то уже недостаточно просто вернуть новые родительские объекты — как только происходит создание нового объекта проекта то все его задачи станут ссылаться на старый объект а это значит что нужно будет пройтись по задачам и обновить у них ссылку на новый объект проекта. Но просто обновить ссылку вы тоже не можете — mapStateToProps не увидит изменения и не перерисует компонент, поэтому нужно пересоздать новый объект каждой задачи проекта. Тоже самое с юзером и проектами. В итоге получится что как только объекты будут ссылаться на родительские то обновление одного объекта приведет к рекурсивному пересозданию всех объектов всего состояния. И если в простых приложениях ссылаться на родителя требуется нечасто и можно обойтись айдишником то в сложных приложениях где одни и же же данные могут отображаться в разных местах, а также в случае many-to-many связей проблема становится все актуальней и проще хранить все состояние в нормализированном виде и всегда ссылаться по айдишникам.
По большому счету все стейт-менеджеры решают одну и ту же проблему — проблему эффективного обновления компонентов. Иначе можно было бы просто хранить состояние в виде глобального js-объекта а в нем хранить любую вложенность объектов с ссылками друг на друга,
const state = { users: [{ firstName: "...", folders: [ { name: "..", user: <link> projets: [ { name: "..", folder: <link> tasks: [ { project: <link>, name: "", completed: true}, ... ] } ... ] } ... ] }] }
И ссылки на отдельные объекты этого состояния можно удобно передавать компонентам (без всяких джойнов как с нормализованным состоянием в редаксе) и также удобно обращаться по обратным ссылкам ссылкам (обратите внимание на строчку
<div>in folder: {task.project.folder.name}</div>
)
const App = () => ( <div> <div>name: {state.user.firstName}</div> <div> {state.user.folders.map(folder=><Folder folder={folder}/>} </div> </div> ) const Folder = ({folder}) => ( <div> <div>folder name: {folder.name}</div> <div> {folder.projects.map(project=><Project project={project}/>} </div> </div> ) const Project = ({project}) => ( <div> <div>project name: {project.name}</div> <div> {project.tasks.map(task=><Task task={task}/>} </div> </div> ) const Task = ({task}) => ( <div> <div>task name: {task.name}</div> <input type="checkbox" checked={task.completed}/> <div>in folder: {task.project.folder.name}</div> </div> ) ReactDOM.render(<App/>, rootEl)
а для изменения объекта достаточно просто изменить нужное поле и вызвать функцию rerender() которая вызовет перерендер реакта (diff) всего приложения
import {rerender} from "./main" const toggleTask = (task) => { this.props.task.completed = !this.props.task.completed; rerender(); } const Task = ({task}) => ( <div> ... <input .. checked={task.completed} onChange={()=>toggleTask(task)}/> ... </div> ) //main.js const rerender = ()=>{ ReactDOM.render(<App/>, rootEl) }
Таким образом можно удобно работать с состоянием и стейт-менеджер был бы не нужен но вот беда — реакт не умеет быстро выполнять diff всего приложения и на больших spa-приложениях это будет тормозить. И тут на сцену приходят стейт-менеджеры которые как раз и решают проблему тормозов реакта — они соотносят какие компоненты от каких объектов или даже полей зависят чтобы выполнить обновления только нужных компонентов когда объекты изменятся.
И получается что вполне логично оценивать стейт-менеджеры по тому каких удобств они лишают в сравнении с разработкой без стейт-менеджера используя обычными js-объекты. Редакс решает проблему точечных обновлений за счет иммутабельного подхода что лишает всех удобств работы с ссылками (вдобавок к неудобствам писать деструктуризации и собирать новые объекты в редюсерах). А например mobx не требует иммутабельности и позволяет объектам ссылаться друг на друга а проблему точечных обновлений решает по-другому (и версия с mobx за исключением добавления декораторов к полям на классах объектов ничем не будет отличаться от примера выше с обычными js-объектами)
DarthVictor
31.03.2019 02:08Ну вот насчет айдишников против ссылок, как минимум не уверен. Бывает, что с ними удобнее.
По большому счету все стейт-менеджеры решают одну и ту же проблему — проблему эффективного обновления компонентов. Иначе можно было бы просто хранить состояние в виде глобального js-объекта а в нем хранить любую вложенность объектов с ссылками друг на друга
А что делать, если нужно что-то обновить зная только его id, возможно составной? Например от websoket'а прилетело сообщение вида «обновили поле fieldName, в записи rowId, таблицы recordType».
Нужно проверять все места в сторе где эта запись может быть? Например
- строка в табличной форме
- строка при расчетах в графиках дашборда
- форма на просмотр одной записи
- строка в связанной таблице формы на просмотр одной записи
- модалка с формой на редактирование одной записи
- строка в связанной таблице модалки с формой на редактирование одной записи
- пользователь скрыл эту форму и обновлять ничего не нужно
Да это из реального приложения пример, и да мне оно тоже кажется слегка перегруженным.
Ладно попроще пример, есть два списка на одном экране в них одна из строк пересекается, как понять, что при изменении в одном списке нужно обновлять эту строку в другом списке?bgnx
31.03.2019 05:23А что делать, если нужно что-то обновить зная только его id, возможно составной? Например от websoket'а прилетело сообщение вида «обновили поле fieldName, в записи rowId, таблицы recordType».
Я не упомянул этот момент — когда нужно загрузить данные или получать оповещения от сервера то действительно нужен еще хеш где нужный объект можно вытащить зная его айдишник. Но это не значит что мы должны везде оперировать айдишниками — со всеми объектами внутри приложения по-прежнему можно обращаться через ссылки просто для нужд синхронизации данных с сервером на объект также будет вести еще одна ссылка из хеш-мапы по его айдишнику (и удобней будет вынести добавление ссылки на объект по его айдишнику в конструктор базового класса)
И тогда получив оповещение по веб-сокетам просто вытаскиваем по айдишнику нужный объект и обновляем нужное поле. А поскольку все объекты в состоянии связаны по ссылкам и распределяются по компонентам от рутового компонент до самых вложенных через передачу ссылок на нужные объекты (как я привел в примере выше) то неважно во скольких местах одновременно будет отображаться данные этого объекта (форма, таблица, модалка и т.д) — при вызове ReactDOM.render(<App/>, rootEl) или используя mobx (который выполнит обновление только нужных компонентов) произойдет актуализация интерфейса в соотвествии с изменениями
Нужно проверять все места в сторе где эта запись может быть? Например
строка в табличной форме
строка при расчетах в графиках дашборда
форма на просмотр одной записи
строка в связанной таблице формы на просмотр одной записи
модалка с формой на редактирование одной записи
строка в связанной таблице модалки с формой на редактирование одной записи
пользователь скрыл эту форму и обновлять ничего не нужно
Ладно попроще пример, есть два списка на одном экране в них одна из строк пересекается, как понять, что при изменении в одном списке нужно обновлять эту строку в другом списке?
Определением какие компоненты нужно обновить при изменении одного/нескольких объектов/полей как раз и является главной задачей стейт-менеджеров и есть несколько вариантов их решения. Сам реакт также решает эту проблему — вызвав ReactDOM.render() на рутовом компоненте реакт проверит все биндиги всего приложения и применит нужные изменения к дом-элементам (и поскольку один и тот же объект находится через ссылки одновременно в двух разных списках то после его изменения ничего дополнительно делать не нужно — реакт сам увидит что изменились биндинги у одного и второго компонента списка).
Но поскольку на больших приложениях такое решение не очень эффективно (а также еще тот факт что реакт использует не самую быструю технику диффа и сам по себе довольно медленный) и будет тормозить на большом количестве компонентов то поэтому и появился весь этот хайп иммутабельности в реакте — мол давайте будем при изменении возвращать новый объект вместе с его родительскими чтобы можно было переопределить shouldComponentUpdate и тем самым реакт сможет заскипать лишние поддеревья интерфейса при диффе. Но такой подход перестает работать на сложных интерфейсах когда связанные сущности будут передаваться через айдишники (а это необходимо потому что данные могут находиться в разных местах интерфейса и хранить копии объекта во всех этих местах и синхронизировать изменения еще сложнее)
Потом появился redux который выполняет проход по всем подключенным компонентам и это позволяет в mapStateToProps вернуть объект по его айдишнику а редакс сравнит старую и новую версию через === и в случае несоответствия обновит сам компонент. А поскольку в обоих списках будет находиться один и тот же айдишник то когда произошло обновление состояния redux увидит что только два компонента вернули новый объект и обновит только два компонента.
А вот mobx работает по другому — каждое поле объекта которое используется в шаблоне оборачивается в некий обзервабл — объект который помимо значения хранит еще список подписчиков на этот объект. И когда начинается процесс рендера враппер над компонентом устанавливает в глобальную переменную текущий инстанс компонента чтобы каждое поле-обзервабл к которому происходит обращение внутри шаблона смогло добавить к себе в подписчики текущий компонент из этой глобальной переменной чтобы при изменении своего значения вызвать обновление всех компонентов-подписчиков на это поле. Ну а благодаря js-геттерам и сеттерам обращение к таким полям-обзерваблам и изменения их значения синтаксически ничем не отличается от работы с обычными js-объектами, а отсутствие необходимости в иммутабельности и возможность связывать объекты по ссылкам сводит к минимуму количество неудобств связанных с внедрением стейт-менеджера в проект в сравнении с версией когда используются обычные js-объекты и дифф реактаDarthVictor
31.03.2019 11:08Я не упомянул этот момент — когда нужно загрузить данные или получать оповещения от сервера то действительно нужен еще хеш где нужный объект можно вытащить зная его айдишник.
Да, в целом разумно.
Определением какие компоненты нужно обновить при изменении одного/нескольких объектов/полей как раз и является главной задачей стейт-менеджеров и есть несколько вариантов их решения.
Там был вопрос как понять что, пришедшие с сервера новые данные частично пересекаются с имеющимися и для них нужно использовать уже имеющиеся ссылки.
Так что тут скорее так же нужно для всех новых данных бегать по хешу. Конструктор объектов придется заменять фабрикой (потому что при создании объекта нужно проверять нужен ли новый или уже имеющийся объект), но в целом должно работать.
А ну еще при использовании id-шников сталкиваемся с проблемой удаление старых данных, прям как в Redux. По идее нужен счетчик обратных ссылок.
kurt_live Автор
31.03.2019 09:28Я не буду целиком разбирать ваш комментарий, просто скажу: можете ли вы циклические ссылки сериализовать? думаю, нет. JSON.stringify у вас просто свалится с ошибкой. Передать при серверном рендеринге с сервера? тоже нет? Понятно.
Видите ли, реакт как и редакс — это ужасные инструменты для изоморфных приложений. В них все может быть сериализовано, а после десериализовано — и будет работать также как и до этой операции. Девтулс редакса работают по тому же принципу.
Видимо разработчики посчитали, что наличие серверного рендеринга лучше, чем каша из циклических ссылок. Кстати, циклические ссылки — это антипаттерн. Вебпак из-за них падает, а GC не может сделать свою работу, и это приводит к утечкам памяти. И я не верю в то, что у вас все идеально и ничего не течет, уж простите.
Кстати, mobX работает на сервере? Никогда не встречал еще.
vintage
31.03.2019 10:03можете ли вы циклические ссылки сериализовать?
Да.
JSON.stringify у вас просто свалится с ошибкой.
Не удивительно, ведь JSON не умеет в графы.
Передать при серверном рендеринге с сервера?
Да запросто. Я даже из субд получаю сразу с циклическими ссылками.
наличие серверного рендеринга лучше, чем каша из циклических ссылок.
Я не улавливаю, чем циклические ссылки могут помешать серверному рендерингу.
Вебпак из-за них падает
Есть сборщики, которые не падают.
GC не может сделать свою работу
Вообще-то GC именно для циклических ссылок и придуман. Без них достаточно было бы лишь умных указателей.
я не верю в то, что у вас все идеально и ничего не течет, уж простите.
А вы редаксовый стор как чистите?
mobX работает на сервере?
А что же ему может помешать работать на сервере?
kurt_live Автор
31.03.2019 21:00
Пожалуйста, напишите какие инструменты и бд, сборщики вы используете. Очень интересно. Звучит как кулстори, если честно.
Я могу написать сериализацию-десериализацию ссылок на js, только это оверхед. Это по факту даже сериализацией ссылок не назовешь. Встречаешь объект, кладешь его в массив, встречаешь его еще раз — присваиваешь ему хэш, вместо ссылки ставишь хэш. При десериализации делаешь это в обратном порядке. Выстрелить себе в ногу — без смс и регистрации.
Гарбадж коллектор придумывался для автоматической чистки памяти в целом, и циклические ссылки — это лишь 1 из задач с которыми он должен уметь работать, в отдельных версиях ie GC не мог в циклические ссылки, так что заявление — что он для них и придумывался — сомнительно.
Как я чищу стор? Легким движением. :)
А что же ему может помешать работать на сервере?
Не видел примеров использования на сервере за три года. Может скудная документация.
mayorovp
31.03.2019 22:43в отдельных версиях ie GC не мог в циклические ссылки
Уточнение: он не мог в циклические ссылки если где-то в цикле был COM-объект, т.е. внешняя неподконтрольная GC сущность. Во всех остальных случаях даже ie с циклическими ссылками справлялся.
vintage
01.04.2019 01:24какие инструменты и бд, сборщики вы используете.
https://orientdb.com/
https://github.com/eigenmethod/mam
Как я чищу стор? Легким движением. :)
То есть не чистите?
Не видел примеров использования на сервере за три года.
Ну вот mam сборщик использует похожую штуку на сервере, чтобы пресобирать бандлы при изменении тех файлов, от которых эти бандлы зависят.
kurt_live Автор
01.04.2019 07:27То есть не чистите?
Чищу. Извините, это был ответ, симметричный вашим:
Да.
Да запросто.
Есть сборщикиОчистка стора делается просто. Вместо того, чтобы в объединенный редьюсер передавать предыдущее состояние, при определенных экшенах подаем undefined. в итоге получаем исходное состояние.
Ну вот mam сборщик использует похожую штуку на сервере
именно mobX? или просто обсерверы? Мне нравится, что вы продвигаете инструменты eigenmethod, но это все равно не mobX на сервере в продакшене.
vintage
01.04.2019 10:20Вместо того, чтобы в объединенный редьюсер передавать предыдущее состояние, при определенных экшенах подаем undefined. в итоге получаем исходное состояние.
Насколько я понял это очистит вообще весь стор, а не только удалит не нужное.
именно mobX? или просто обсерверы?
А MobX чем-то принципиально отличается?
kurt_live Автор
01.04.2019 15:02Насколько я понял это очистит вообще весь стор, а не только удалит не нужное.
По вашему вопросу не понятно, совсем чистить или не совсем.
А MobX чем-то принципиально отличается?
Другая библиотека? MobX стейт менеджер, а mam — нет? MobX является частью экосистемы, а mam или просто обсерверы — нет? Достаточно принципиально?
vintage
01.04.2019 15:51А зачем в приложении чистить стор совсем?
mam — сборщик, использующий библиотеку $mol_atom, аналог MobX.kurt_live Автор
01.04.2019 17:36+1при логауте, например.
mam — сборщик, использующий библиотеку $mol_atom, аналог MobX.
супер! но тем не менее, это не mobX… Вопрос то был про то, зачем редакс если есть mobX. Ответ был про заточенность на серверный рендеринг, и про отсутствие примеров серверного рендеринга с mobX. И опять же, пусть мы берем mam в расчет: 12 звезд против 18.9k у mobX, 47.8k у redux. $mol — 200 звезд… Библиотека может быть сколько угодно хорошей, но пока нет сообщества, экосистемы — нет и специалистов, не спроса у продуктовых и аутсорс компаний… Вам лучше пиарить эти инструменты на митапах, вебинарах и конференциях, написать больше статей, примеров и прочего. Собрать сообщество и экосистему…
Давайте свернем это обсуждение. А то получается такой диалог:
я: мне нужна ложка, у вас есть?
вы: есть вилка!
я: но мне нужна ложка!
вы: есть вилка и палочки, они совсем как ложка, удобные! зачем вам ложка?
я: хорошая вилка и палочки для суши наверно удобные, но мне нужна ложка — я ем суп!vintage
01.04.2019 17:58При логауте нужно всю страницу перезагружать, чтобы гарантировать, что ничего не останется в памяти, ни в каком замыкании.
Вы не суп едите, а в аппельсинах ковыряетесь. Нет абсолютно никакой проблемы использовать мобх на сервере. Он гвоздями к фронтенду не прибит. Точно также и у редакса нет абсолютно никаких специальных заточек под серверный рендеринг.kurt_live Автор
01.04.2019 18:41+1При логауте нужно всю страницу перезагружать, чтобы гарантировать, что ничего не останется в памяти, ни в каком замыкании.
зачем? может еще и старый комп сжигать, чтоб наверняка?
как только вы выгружаете страницу из памяти, например, делаете логаут, и очищаете стейт у вас не останется ничего… это конечно, если вы не злой буратино и не пишите специально сохранение всяких левых данных…
vintage
01.04.2019 19:47-1Ага, а потом с помощью таймтревела злоумышленник восстанавливает весь стейт.
DarthVictor
01.04.2019 20:44Злоумышленник при наличие доступа к учетке просто перезайдет с сохраненным в браузере паролем. Или килоггер поставит
vintage
01.04.2019 22:58Кейлоггер без пароля админа не поставить. Ну и сохранять пароль на машине с которой собираются выйти, никто не будет.
DarthVictor
02.04.2019 00:38Кейлоггер без пароля админа не поставить.
Пишут, что работает. Не знаю правда работает ли в этом режиме дневник ввода, но думаю, что да. Раньше на него даже антивирусы не ругались.
Итого для такого вектора атаки необходимо:
1) Чтобы был доступ к машине после пользователя, но не было до этого.
2) Но чтобы было время прогонять js-скрипты в консоли.
3) И чтобы пользователь боялся сохранять пароли в браузере.
4) Но не боялся заходить под чужой учёткой.
5) И не закрывал после этого вкладку.
И атаковать нужно пользователей можно строго по одному.
Так себе вектор для атаки.
P.S. iframe для формы логина работает не хуже, но при этом защищают логин/пароль/данные кредитки от такого вектора атаки как XSS. Который вполне себе реальный способ атаки.vintage
02.04.2019 09:07Вы как-то однобоко смотрите. С чего вы взяли, что атаковать нужно больше одного пользователя? Что приватные данные — это только логин и пароль?
По кнопке "выход" нужно принудительно выгружать страницу именно для того, чтобы в памяти гарантированно ничего не осталось. Не доверяете мне — спросите любого знакомого безопасника. Больше по этой теме мне добавить нечего.
DarthVictor
02.04.2019 11:15По кнопке «выход» нужно принудительно выгружать страницу именно для того, чтобы в памяти гарантированно ничего не осталось.
Вы таким образом не почистите: cookies, localStorage, indexedDB, историю браузера, кеш браузера.
Почему Вы думаете, что забыть убрать девтулзы с продовой сборки можно, а забыть убрать ветку пользовательских данных из серриализатора стора в localStorage — нельзя? Я вот на второй кейс не так давно наталкивался.
Единственное что ваш подход с перезагрузкой страницы дает — ложное чувство безопасности. Типы мы заботимся о безопасности и закрыли одну дырку, аудит безопасности можно не делать. Пофиг, что аудит безопасности найдет ещё 99 дырок.
Что приватные данные — это только логин и пароль?
Во-первых, я упоминал еще данные кредитной карты.
Во-вторых, при наличии данных для входа многие другие данные можно восстановить при помощи истории браузера.
SayLovePlz
30.03.2019 20:30А пока кто-то ставит звездочки пакетам, которые проверяют «а не 13 ли ты часом»
ахаха это пять)))
amakhrov
30.03.2019 21:44+3Вы работаете на новом для вас проекте, или ваш коллега писал некую функциональность год назад, а вам теперь ее нужно расширять
Год назад коллега написал нужную фичу в (условно) 2-3 строчки кода (
fetch(url).then(() => this.setState())
).
Через год требования поменялись и надо провести рефакторинг — перенести данные в глобальный стейт.
По-моему, ситуация стандартная и беспроблемная. Более того, сейчас, зная новые требования, вы формируете данные в глобальном стейте сразу в нужной форме.
А могло статься, что этой новой фичи бы не было — и старый код работал бы без проблем еще год.
Имхо, проблема только одна (как и указано в статье) — ломается тайм тревел.
И тут уже надо смотреть по ситуации, насколько полезен тайм тревел для простого случая (загрузили данные -> показали на экране) по сравнению с оверхедом, который он привнесет.kurt_live Автор
30.03.2019 22:07Год назад коллега написал нужную фичу в (условно) 2-3 строчки кода
Я не работаю на таких мелких проектах… Было бы счастьем считать, что в реальных приложениях только такие фичи и больше никто ничего не трогает и не добавляет.
только одна
в статье написано больше, однако…
amakhrov
31.03.2019 00:07только такие фичи
А я и не говорю про "только такие". Но такие тоже есть. Более того, даже в сложных фичах зачастую есть элементы, где какие-то данные загружаются и используются единожды (что вписывается в схему fetch.then(setState)).
Вы в статье призываете всегда использовать редакс вместо состояния компонента — даже для подобных случаев.
Я же утверждаю, что есть сценарии, где состояние компонента более уместно.
в статье написано больше
В статье есть секция "проблемы" из двух пунктов. Первый — react dev tools — по-моему, какой-то очень граничный случай. Я, например, никогда с этим не сталкивался. Остается только проблема с тайм тревел.
kurt_live Автор
31.03.2019 00:30Секция действительно из 2х пунктов, однако проблем больше:
У нас появляются всякие техники типа lift state up (это когда данные нужны сиблингу хост элемента, поэтому часть state и всю логику работы с ней выносят в родителя, тратя уйму времени на переписывание и тестирование рабочей логики) и т. д. Появляются баги, оверхед в доработках и много-много радости.
в редакс данных нет. В коде нет экшенов, редьюсеров, что хранят нужные вам данные. И вы начинаете путешествие по дереву компонентов поисках заветного и находите их (!!!) даже несколько штук.
основная проблема наличия internal state — он конкурирует с redux за данные, проигрывает в долгосрочной перспективеПостараюсь лучше структурировать статьи впредь.
Вы в статье призываете всегда использовать редакс вместо состояния компонента
Это где? Вот что я пишу:
Не используйте вместе с redux других хранилищ.
И уже потом, подразумевая, что вы все-таки используете redux с react написал:
Храните данные только в redux(все-все, даже «внутренние»), если он подключен к вашему react-проекту в качестве основного хранилища, не используйте хранилищ которые будут конкурировать с ним.
А еще я пишу, что отказался от redux в пользу pubsub и внутреннего состояния для конкретной цeли. Недостаточно иметь инструмент, важно знать когда и как его правильно использовать.
Основной посыл в том, что при использовании конкурирующих технологий портится ПО… и привел конкретный пример таких технологий. Я не рассматривал здесь недостатки той или иной библиотеки, а только их соединение вместе. И вот на стыке мы имеем проблемы, которые я и освещал.
amakhrov
31.03.2019 01:28lift state up
В этот момент может быть целесообразно вынести состояние из компонента в редакс. До этого такой необходимости не было. Еще раз — я говорю про случаи, когда какие-то данные не переиспользуются в разных компонентах. Ну просто требования таковы, что данные загружаем и показываем в одном месте. В моей практике такое то и дело встречается (не преимущественно, но достаточно часто, чтобы заслуживать упоминания)
начинаете путешествие по дереву компонентов
Я обычно с этого и начинаю. Смотрю, откуда компоненту пришли эти данные (предполагаем, что с кодовой базой я не настолько хорошо знаком). Так все равно проще, чем искать данные в развесистом дереве редакс (развесистое — ибо используется по всему приложению).
По сравнению с глобальным состоянием, хранение данных в компоненте имеет преимущество локальности:
- загрузка данных и их использование описаны в одном месте — проще прочитать, проще поправить
- удаление фичи тривиально — удаляем компонент
- есть гарантия, что эти самые данные никто больше не использует! если у вас нет строгой типизации в проекте (допустим, с typescript), может быть непросто понять — насколько безопасно будет удалить или поменять данный кусок глобального состояния.
Вот это преимущество локальности конфликтует с тайм тревел, который основан на едином глобальном состоянии. И тут уже нам надо выбрать — что нам важнее в данном конкретном сценарии.
kurt_live Автор
31.03.2019 09:01Когда вы переделываете(тут даже не переделываете — тут отрезали, там приклеили) то, что работало, чтобы просто добавить новый функционал — это ужасно. По сути вы не делаете полезную работу в этот момент, а наоборот. Это говорит о том, что вы не продумывали тот код который написали, как архитектор. Причины неприятия такого подхода я описал: нагрузка на всю команду растет, возможны баги и прочее — то есть все работают больше, а выхлоп маленький. А со временем и он сойдет на нет. И тут вопросы "почему ты трогал одно, а сломалось другое?" вполне актуален. Вы не учитываете этот момент.
И да, зачем вам редакс, если вам проще использовать внутреннее состояние, не понятно. Я не настаиваю на том, чтобы вы отказывались от внутреннего состояния. Я говорю о том, что они с редакс конфликтуют, и чтобы избежать конфликта и нестабильности нужно не использовать 1 из вариантов. Но поскольку я начал с того, что "нельзя просто так взять и использовать редакс", то и закончил с позиции редакса. Этого требует целостность повествования. Отказаться от редакса — тоже решение данной проблемы.
amakhrov
01.04.2019 05:08Вы приписываете мне то, что что я не говорил.
Я не утверждаю, что мне проще всегда использовать внутренее состояние. Я говорю, что в ряде случаев локальное состояние более целесообразно.
Редакс мне для хранения общего состояния. Залогиненный юзер, пермиссии, высокоуровневые данные, используемые в нескольких частях приложения. А также данные, нужные только на одной странице, но используемые в различных ее частях / на разной глубине дерева компонентов.
Локальный стейт мне для одноразовых данных. И еще для состояния интерфейса, которое живет только на фронтенде (например, флаг "открыт ли выпадающий список").
kurt_live Автор
01.04.2019 07:37Простите, если понял вас неправильно.
Еще раз повторюсь: предмет статьи в том, что 1й постулат редакса не работает с react internal state. Отсюда вытекают дополнительные сложности.
Вы в своем первом комментарии написали про получение данных с сервера и сохранение его в локальный стейт — это уже противоречит локальному состоянию, которое вы описываете дальше. Так по вашему нормально данные с сервера сохранять через замыкание в state? Вам не кажется такой подход противоречивым и требующим пересмотра?
amakhrov
01.04.2019 08:29Приведу конкретный пример — для более предметного обсуждения.
Задача
На странице данные, связанные с различными твиттер аккаунтами (можно добавить какой-то аккаунт в список, указать к нему настройки, специфичные для приложения, batch-операции со списком).
И есть доп возможность — новые аккаунты в список добавлять не вручную (ввод с автодополнением), а добавить полностью список, привязанный к твиттер-профилю текущего пользователя (https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-list.html).
Выглядит примерно так:
- пользователь жмет кнопку "выбрать твиттер-списков"
- открывается модальное окошко, в котором через апи твиттера загружается коллекция этих список
- пользователь выбирает один из них
- еще один апи запрос на получение конкретных твиттер-аккаунтов из выбранного списка
- твиттер-аккаунты добавляются в основной список на странице.
Коллекция твиттер-списков используется в приложении один-единственный раз. Более того, эти данные временные — после того, как пользователь сделал свой выбор и получил на выходе коллекцию твиттер-аккаунтов, эти данные становятся не нужны. При повторном открытии модального окна я все равно ожидаю увидеть актуальные данные из моего профиля твиттер, а не закэшированные с прошлого раза.
Хранение в стейте
Так вот, эти одноразовые данные, используемые только для чтения, я с бОльшим удобством храню прямо в локальном стейте компонента, который реализует отображение этой коллекции твиттер-списков и выбор одного из них. А не в общем редакс-стейте приложения.
Плюсы локального стейта я перечислял выше — вся логика в одном месте, проще и безопасней вносить изменения. Инкапсуляция, вот это все. Ну, не мне вам описывать преимущества локальных переменных над глобальными. Вдобавок меньше бойлерплейта.
Да, тайм тревел не отобразит данные в этом модальном окне немедленно (вместо этого, тайм тревел приведет к маунту компонента, что, в свою очередь, вызовет запрос к апи твиттера).
Поскольку логика тут примитивная (апи запрос -> показали на экране), необходимость отладки именно этого куска через тайм тревел тут маловероятна.
Я допускаю, что в отдаленном будущем возможно такое изменение требований, которое потребует сделать эти одноразовые данные многоразовыми / используемыми в другим местах. Но зная примерный курс развития продукта, я оцениваю вероятность этого как весьма низкую. И если даже это случится — ничего сложного в переносе данных в редакс я не вижу.
Какие конкретные минусы в данном подходе видите вы?
kurt_live Автор
01.04.2019 15:19Я, если честно, вижу тут следующее:
Пользователю с медленным 3g придется ждать вечность, пока подгрузится список. он закроет окошко не дождавшись...
Как поведет себя ваше приложение? Упадет при резолве ответа, ведь компонента больше нет? При повторном открытии выкинет данные и пошлет опять запрос и пользователь опять не дождется?
У компонента всегда есть родитель, который может сказать вашему компоненту "до свидания". Тем самым весь прогресс теряется и начнется проделывать все заново. Чтобы сделать актуальность — достаточно запрашивать не чаще чем 5-10 секунд с прошлого ответа.
При этом стор как правило живет, пока живет приложение. Соответственно: если запрос был секунду назад или еще в процессе вы и так получите актуальные данные. И не важно сколько раз тапнет пользователь. При этом сам компонент не будет отвечать ни за что кроме рендеринга — это основное назначение компонента, а не запросы к серверу.
Есть понятие зона ответственности, ее не нужно нарушать, тогда приложение будет поддерживаемым, простым, тестируемым и более надежным.
Поймите, у меня вопросов к редакс, наверное, больше, чем у многих, и я его не выгораживаю, но я имею четкое понимание, что если не разделять ответственность, не выделять абстракции, слои — приложение превращается в кучу кода, а не в ПО.
amakhrov
02.04.2019 06:56Разделение ответственности и использование глобального / разделяемого состояния — ортогональные концепции.
Нет никаких проблем отделить презентационную часть (разметку) от логики — при этом логика будет работать с локальным стейтом контейнера, а не с глобальным.
Все, что вы написали выше, без проблем реализуется на локальном уровне.
В чем преимущество использования именно глобального состояния для этой задачи?
Заметьте, я не пишу конкретно про редакс. Замените его на Mobx, любой другой Flux или что-то самодельное — ничего не поменяется. Противопоставление именно глобального состояния и локального.
Я оспариваю именно ваш призыв использовать везде глобальное состояние и избегать локального.
kurt_live Автор
02.04.2019 10:34Я написал какие проблемы. Хотите еще? Окей, допустим вы вынесли локальное состояние и получение данных выше, чтоб наверняка, в контейнер страницы. У вас появилась необходимость прокидывать пропсы через иерархию. Это приводит к связанности компонентов и ререндеру для прокидывания нового состояния. То есть компоненты, которые организуют передачу будут вызывать рендер, чтобы ваш список получил данные, а не чтобы перерисоваться. Все начинает притормаживать — реакт в фоне делает пересчеты. Как итог, вы получаете свои данные, а пользователь — тормозящий интерфейс.
Расскажу вам кулстори 2х летней давности. Попал я на проект одной иностранной соцсети. И был там чатик. Все бы хорошо, но чат был просто отвратительно написан. я тут в комментариях писал про редьюсер в 1600+ строк — это как раз редьюсер чата. рефакторинг занял у меня 4 дня, я удалил более 11000 строк суммарно из кода чата и связанных модулей.
В чем с ним была проблема? Ну кроме спагетти кода, написанного крайне небрежно, мутаций и прочего.
Если открыть чатик в хроме — он работал, притормаживал, но вполне жил, скрепя байты, делал свою работу. Турбофан все же делает свое дело...
Если открыть в фф и попытаться написать — символы появляются с задержкой до 6 секунд. А если пишут тебе — то интерфейс становился еще мертвее.
Проблема была в посылке статуса тайпинг, хотя это и не единственная проблема. Просто кто-то жмет клавишу — а у тебя окно чата 627 раз зовет рендер (я написал простенький счетчик вызовов, чтоб разобраться). И вот если у тебя не мощный комп, а тогда я ради эксперимента пересел на ноут из днс с пентиумом, ты превращаешься в хатико. Ты резко ненавидишь разрабов, которые сидят на своих компах с топовым железом и не думают про планшеты и дешманские ноуты. А ведь если думать о пользователях соцсетей — они выходят в сеть с чего угодно, но не с компа с 6-ядерным intel восьмого поколения и 32 гигами оперативки.
Так вот, оказалось, что этот самый "user typing..." (а точнее флаг) пропс дриллингом прокидывался через иерархию компонентов к месту вывода. Ну и получается суммарно 627 рендеров на нажатие клавиши у твоего оппонента. Стоило вынести тайпинг в стор и конектнуть именно строку которой нужен флаг, и лаг снизился раз в 5. а количество ререндеров стало 1.
Поэтому я все понимаю, подход ваш тоже, но использование редакс, как и неиспользование, может привести к страшным последствиям, если не думать.
vintage
02.04.2019 11:12-1Это приводит к связанности компонентов и ререндеру для прокидывания нового состояния. То есть компоненты, которые организуют передачу будут вызывать рендер, чтобы ваш список получил данные, а не чтобы перерисоваться.
То есть основной архитектурный принцип Реакта является полнейшей глупостью. Но вы продолжаете колоться, плакать и продолжать
есть кактусписать костыли.kurt_live Автор
02.04.2019 11:29Что вы злой такой? При чем тут костыли? Любая библиотека имеет недостатки и требует затрат для изучения, правильного использования и оптимизации, а все разработчики допускают ошибки в планировании и разработке — они люди, разработчики библиотек тоже, как ни странно. Непогрешимых нет. Другое дело делают ли они работу над ошибками и учатся новому, или нет. Вы тут хейтите по чем зря. Вашу позицию выяснили — мы глупые, жрем кактус, однако по щелчку только в комиксах мир меняется. Такое ощущение, что вы существуете в отрыве от реальности. Не надо так.
vintage
02.04.2019 12:38Попробуйте для разнообразия вместо того, чтобы выискивать личные оскорбления, подумать о чём собеседник говорит. Вы сами признаёте фундаментальную проблему дизайна Реакта, из которой вытекают почти все проблемы при работе с ним. Единственная работа над ошибками, которая тут возможна — выпиливание Реакта полностью и использование чего-то иного. Да хоть тот же Vue или Svelte из популярного имеют куда более продуманный дизайн, чем этот уродец из
МордораФейсбука.
Nermo
31.03.2019 20:13Храните данные только в redux(все-все, даже «внутренние»), если он подключен к вашему react-проекту в качестве основного хранилища, не используйте хранилищ которые будут конкурировать с ним.
С таким советом нужно быть очень аккуратным. Посмотрите доклад Козули на Moscow Frontend Conference 2017 youtu.be/ZijpBIO452w?t=755
Они там тоже все в сторе хранили, и кончилось все очень плохо, пришлось все обратно из стора в локальный стейт выносить.kurt_live Автор
31.03.2019 21:07Как я писал уже выше в ответе на один из комментариев — я начал повествование про редакс, соответственно выводы были с его стороны. Откажитесь от редакс — это тоже решение. Опять же смысл статьи не в том, чтобы призвать всех хранить в редакс, а в том, чтобы показать несостоятельность постулата и конфликт.
faiwer
01.04.2019 15:55Особых проблем с разделением state & store у меня не было. Кроме как в самом начале, пока разбирался.
А вот когда к уже готовому приложению попробовал приделать router, тут я поплыл. Судя по всему подход redux не совместим с самим принципом наличия ещё 1 полноценного источника правды (URL-address). Любые изменения в маршрутизации могут потребовать тонну рефакторинга и костылей. А если в качестве роутера ещё какой-нибудь ReactRouter...
Для себя сделал вывод — если у приложения предполагается рутинг — уделить ему предельно много внимания на самых ранних стадиях. Иначе будет много боли.
vintage
01.04.2019 16:04Вот смотрю я сколько у людей проблем с роутингом что на ангуляре, что на реакте, и недоумеваю, зачем всё так через жопу делать. Это ж тривиальная штука показывать разные вещи в зависимости от текущего урла.
faiwer
01.04.2019 20:08В React я думаю проблем нет. Речь именно про Redux. Сама философия располагает к тому, чтобы всё важное было в store. А store штука непростая. Он должен быть устроен мудро. Рефакторинг структуры стора — боль. И когда в приложении появляется ещё 1 полноправный источник истины возникает вопрос как это всё синхронизировать. Да ещё и так, чтобы не нужно было продумывать рутинг в мелочах ещё до создания остальной архитектуры приложения. И вот тут прямо всё плохо. Скажем захотеось добавить какое-то поле в URI-параметры, чего раньше в них не было, привет большие проблемы. Очень много всего переделывать, включая, вероятно, и тесты.
В общем я пока не понял как правильно готовить сложные SPA с нормальной маршрутизацией в условиях часто меняющегося ТЗ. С каким-нибудь observable там легко. Обычно observable = бардак и нет никакой сложной фиксированной иерархии. Стало быть и нет проблем этой иерархии.
kurt_live Автор
02.04.2019 10:18Согласен с вами, организация роутинга в приложении с редакс вызывает проблемы. Конечно стало несколько проще с появлением 4 версии роутера, но проблем это не решило.
rockon404
02.04.2019 01:23Храните данные только в redux(все-все, даже «внутренние»), если он подключен к вашему react-проекту в качестве основного хранилища, не используйте хранилищ которые будут конкурировать с ним.
Пожалуй, самый плохой совет, который только можно дать начинающему разработчику. Еще не было ни одного кейса за мою практику работы с React и Redux, где разделение store & component state вызывало бы какие-либо проблемы. Возможно, проблема существует лишь в вашей голове, а отсутствие code review в вашей команде вносит в это свою лепту.
Настоятельно не рекомендую следовать совету автора. Почитайте лучше, что пишет сам автор Redux по этому поводу.kurt_live Автор
02.04.2019 10:09Так статья не для начинающих(о чем я в начале написал). Это не призыв, а проблемы я описал. Но видимо каждый замечает то, что хочет и не смотрит на остальное… Это хорошо, что вы приводите ссылку на комментарий в issue, но хотелось бы пояснений в документации или более популярных источниках.
Проблема, которую я описал, а именно конфликт состояний и поломку таймтревела описана в документации redux тремя строками. Локальный стейт используют не думая и не разбирая. Редакс используют так же не разборчиво. в цитате, которую вы приводите, есть уточнение, которое как по мне снимает претензии
DarthVictor
Вы в статье в основном разбирали только одну проблему редакса — сложности синхронизации с внутренним стейтом реакта. Могли бы описать как ваша PubSub эту проблему решает? Потому что, по-моему эта проблема согласованности данных, и к реакту/редаксу она отношения не имеет.
Например что изменится, если в вашем абзаце
я поменяю «redux» на «PubSub»?
Кстати, чем вам не подошли и имеющиеся PubSub библиотеки, тот же MobX?
После двух лет работы с Redux мне в числе его проблем наиболее важными показались проблемы с продвинутыми руководствами для крупных проектов и недостаточное освящение таких проблем как нормализация данных, разделение стейта страницы и стейта отдельных компонентов, обязательная работа с отдельными элементами через идентификаторы, отделение часто меняющихся данных в отдельные ветки.
В итоге многие хранят в стейте список из 100 элементов тупо массивом и удивляются, когда при изменении одного перерендериваются все 100. А если там у элемента еще и поля для ввода…
kurt_live Автор
Я писал Вам такой развернутый ответ, но при попытке вставить debug скриншот все улетело.
Попробую повторить, но возможно, что-то упущу.
Я писал не об этом, а о том, что на самом деле 1 постулат не соблюдается, что внутреннее состояние конфлитует и конкурирует с redux, что вместе их использовать нельзя. C redux не нужно использовать другие хранилища. Тот же timetravel не предполагает использование внутреннего состояния и ломается, потому что внутреннее состояние зачастую изолировано от входящих пропс.
Изолирует redux от react internal state, служит транспортом и обеспечивает подписку на конкретные данные, вместо подписки на целое состояние. каналы могут быть использованы как отдельно, так и объедены в один, чтобы доставить непосредственно в стейт компонента данные. Да, сабскрайбер пишет в state.
Redux и React internal state конфликтуют, ломают функциональность друг друга. Допустим, мы еще можем сказать, что Абрамов написал библиотеку универсальной без прицела под React, интеграцию писали другие люди, но facebook приняли Дэна на работу, а redux стал стандартом в реализации flux архитектуры, но вот о том, что они конфликтуют и нужно это учитывать, пишу я, а не Дэн. Если же я просто не читал или не видел такого, а такие заявления есть — поделитесь ссылкой, буду признателен и посыплю голову пеплом.
Вы конечно можете, но это перестанет быть правдой. Я написал выше почему.
Отсылка к библиотеке isThirteen вас нисколько не удивила? Объясню: зоопарк решений, зачастую просто неоправданных. В одном проекте и двух-то стейт менеджеров много, а вы спрашиваете почему я не взял третий?
Свой pubsub я использовал, так как он предсказуемый, маленький и мой. я подогнал его под конкретную задачу и смогу использовать в других местах, проектах и т.д. я не говорю, что мой pubsub лучше mobx, разве что он специальный, а mobx или любой другой публичный pubsub пишутся с оглядкой на более широкое и универсальное использование.
Это грустно читать. Руководств такого толка как вы хотите вряд ли будет, хотя есть real-world-app с кучей вариаций, но это примеры, а не руководство. Мой личный рекорд ужаса в проекте с redux — это reducer со switch на 1600+ строк. С мутациями, циклами… в общем, рефакторинг был просто незабываемый.
У редакса много "болезней" и я уже больше года придумываю разные решения для их исправления. Например, избавиться от switch, сделать reducer декларативными, не использовать react-redux для интеграции и многое другое. Я не зря написал, что это первая записка, я буду развивать эту тему и хочу добиться того, чтобы выгоды использования redux не терялись за его недостатками.
DarthVictor
А можно воспроизводимый пример? Как именно они ломают функциональность друг друга? За два года ни разу не видел. Я много видел, когда одни и те же данные хранят и в сторе и в стейте и они идут в рассинхрон. Но именно чтобы две независимых свойства конфликтовали — не видел ни разу. Стор это же просто несколько разных пропсов они могут откуда угодно прийти. Хоть от redux, хоть от mobX, хоть от сторибука.
Честно говоря, не сталкивался, может повезло, у меня timetravel просто не трогал собственный стейт компонента. И вполне логично что стейт реакта нужно менять в расширении реакта, а стор редакса — в расширении редакса.
Если ли же у вас внутренний стейт может влиять на стор (напрямую или через пересоздание дочернего компонента и влияния на его методы жизненного цикла) — выносите его в редакс, Flux специально придуман, чтобы не разруливать всю эту синхронизацию.
Ну вообще у Абрамова в официальной доке всё это есть. Про не использование массивов чего-то сложнее id-шников даже видео есть. Просто все как всегда читают в документации первые 2-3 главы, а потом удивляется от чего их приложение тормозит.
Ну ветки swtich либо независимы (ловится линтером через стандартные правила вроде no-case-declarations, no-fallthrough) и тогда там хоть миллион строк может быть, либо даже не знаю данные экшена или стора там что ли мутировали? Тогда как это PR прошло? Выглядит, что Вам просто какой-то говнолегаси от джунов после трехмесячных курсов спихнули.
kurt_live Автор
Воспроизводимый пример есть у меня в проекте, и если у меня будет время — я его приведу в измененном виде. но механизм я привел в статье:
Это и есть "ломать".
Курс Абрамова он настолько базовый, что до реального приложения — там как до луны. Я честно не видел массивов в виде стейта в приложениях. Огромной вложенности объекты — да. Но видел, как джуны путают массивы и объекты, это да.
Знаете, для меня switch как goto… Применять его не умеют нормально. Нет, он конечно иногда удобен, но я сторонник декларативного подхода. Если ты по одному полю выбираешь, что делать — используй объект. Да, я знаю, что объект кастует ключи в строки — тут это самое то. Но функция куда лучше ветки свича. Никакой линтер не справится лучше.
Насчет джунов: нет, люди там пишут на js годами. Но подход а ля процедурное программирование или jquery-style в реакт они привнесли. Потому что так привыкли.
kubk
— Организация reducer'а через стандартный класс
— Redux-symbiote — пишем действия и редьюсеры почти без боли
— Redux — пересмотр логики reducer'a и actions
— Оверинжинирг 80 уровня или редьсюеры: путь от switch-case до классов
Быть может есть смысл рассмотреть другие библиотеки для управления состоянием, например MobX, где тоже есть логгирование, девтулзы, но не нужно городить над библиотекой свои обёртки, меняя привычные подходы до неузнаваемости, как это сделано в статьях выше.
kurt_live Автор
Я ни в чем не уверен.
Еще одно предложение про mobx) Смотрите почему я не хочу лезть в mobX — опыта нет. Могу или перехвалить, или охаять. Все-таки его нужно использовать, чтобы давать оценку, выявлять несуразности и минусы, которых стоит избегать. С redux я работаю три года — все три года на него не падает спрос, проектов было достаточно как с нуля, так и в очень запущенном состоянии. Я делюсь опытом. В реинжиниринге каких-то вещей, переосмыслении — не вижу ничего плохого. так работает прогресс. Возможно, накопленная критическая масса подтолкнет кого-то создать лучший фреймворк.
fAtliNg
Конечно если редьюсер написан таким образом, что он просто возвращает новый список, то перерендрится весь список. Но проблема с перерендриванием частично решается простым return state.map(...). В .map вставляем конструкцию if/else, если элемент из state равен элементу из action.payload, то возвращаем элемент из state, иначе возвращаем из action.payload.
Конечно проблемы в таком случае возникают, например если приходится сравнивать глубокие объекты (ну придется заюзать deepEqual, вроде решаемо), более серьезная проблема связана с тем, что length нового списка и списка из state может отличаться, в таком случае возможно легче вернуть новый список и пускай все перерендривается (но думаю можно повыпендриваться и что-то придумать).
Поэтому кажется, что способы хранить как массив и как объект имеют место жизни, однако я бы сам предпочел хранить конечно объектом.