През последните 8 месеца моят екип и аз използвахме Microstates.js в нашето приложение Ember.js, за да направим управлението на състоянието по-забавно. Ще ви разкажа какво мотивира нашето решение, какви са били печалбите и ще ви помогна да го направите.

Мотивация

Работя в Allovue, компания за финансиране на образованието, която получава повече пари за ученици от K-12, като помага на държавните училищни райони да разберат, управляват и бюджетират парите си. Тъй като работим с финансови данни, обикновено имаме много таблици в нашия потребителски интерфейс.

Всички над 20 таблици, които показваме на нашите потребители, изискват някои общи функции освен простото показване на редове и колони. Всяка таблица трябва да може да се филтрира, сортира и пагинира. Само с тези 3 функции има доста състояние за управление. Трябва да знаем кои колони трябва да бъдат сортирани и в какви посоки, към кои колони са приложени филтри, какви филтърни оператори се използват, колко елемента на страница да се показват и т.н. За да усложним допълнително нещата, някои от тези части от състоянието зависят от един друг. Например, ако променя посоката на сортиране, трябва да нулирам странирането на страница 1.

Как бихте приложили всички тези функции с помощта на Data Down Actions Up (DDAU)? Можете да го направите, но става малко объркано и по-малко забавно.

Толкова много аргументи

Имаме „контекстуален компонент“ за нашите таблици, който прави възможно предварителното свързване на тези свързани компоненти с необходимите данни и действия. API за този контекстуален компонент обаче е малко тромав. Трябва да предадем всяка част от състоянието заедно с действието за актуализиране на това състояние.

Искаме да покажем части от това състояние като параметри на заявката в URL адреса, за да го направим споделяем между потребителите. Спазването на това изискване означава да притежавате controller.js данните, а не <Table> компонента.

Свързано състояние на групиране

За да намалите броя на отделните действия и състояния, които се предават, може да е добра идея да групирате свързани данни заедно в обект, през който да преминете. Това може да ви даде нещо подобно на това:

Това определено изглежда по-управляемо, но сега има някои нови проблеми:

Поддържане на нещата в синхрон

Нашите клиенти имат много данни, което означава, че не можем да ги заредим всички наведнъж от сървъра. За да поддържаме страницата бърза, трябва да пагинираме данните и да зареждаме само част по част. Сървърът прави пагинацията вместо нас, тъй като знае за всички данни и е по-бърз. Това означава, че ако сортирането се промени или филтърът се промени, трябва да извлечем отново нашите данни. Не искаме целият маршрут да се изобразява отново всеки път, когато състоянието на таблицата се промени, вместо това искаме <Table/> да зарежда собствените си данни и да показва бутон за зареждане, докато това се случва.

За да направим това, поставяме didReceiveAttrs кука вътре в <Table/> и изпълняваме задача ember-concurrency всеки път, когато състоянието се промени.

Всичко това е добре, така че нека опитаме да актуализираме част от състоянието. Да кажем, че искаме да променим посоката на сортиране на колона. Естественото нещо, което един разработчик на Ember може да напише за действието на сортиращия компонент, би било нещо подобно:

Поздравления, току-що сте нарушили DDAU и сте мутирали състояние, което не сте притежавали. Беше твърде лесно да се направи това! Това няма да задейства нашата кука didReceiveAttrs, защото тя се изпълнява само когато препраткатана цял обект се промени, а не само свойство на обекта.

Освен това, ако имате някакви изчислени свойства, които изглеждат така:

Те също няма да се актуализират. Ще трябва да посочите всяка част от зависимия ключ, който ви интересува 'sort.{dir,column}'.

За да накараме didReceiveAttrs да се задейства и да имаме прости изчислени дефиниции на свойства, трябва да заменим ЦЕЛИЯ sort обект, който се предава. И така, как да направим това?

В нашия контролер ще трябва да имаме действие, което прави нещо подобно:

И нашият компонент, който използва това действие, ще трябва да предаде обект като този:

Защо това е толкова трудно? 😫

Всичко, което искам да направя, е да презаредя данните си всеки път, когато състоянието на таблицата се промени. Ако беше само една част от държавата, нямаше да е толкова зле. Въпреки това, реалността в повечето приложения е, че има много повече състояние от това за управление. Това означава, че за всяка отделна част от състоянието в моето приложение, която трябва да се държи по този начин, ще трябва да:

  • Създайте моя обект на състояние и го предайте на моите компоненти
  • Създайте действие, което копира нов обект на състояние и предайте това действие на моите компоненти
  • Използвайте моето действие в дъщерен компонент и му предайте цял обект, вместо само свойството, което ме интересува
  • Уверете се, че всички мои изчислени свойства указват свойствата, които ги интересуват, на обекта на състоянието, вместо само самия обект

Най-лошата част от това е, че дори и да мина през всички тези обръчи, няма гаранция, че друг разработчик няма да направи set(this.sorts, 'dir', dir) и да въведе грешка по-късно. Чувства се много естествено и добре да се променя състоянието по този начин. Очакването на бъдещи разработчици (включително и аз) да не забравят да се противопоставят на този интуитивен API ги настройва да направят грешното нещо.

Микродържавите на помощ

За мое щастие слушам Frontside Podcast и чух за тази много интересна библиотека, наречена Microstates.js, която се занимава с тези проблеми.

Неизменност

Това, което наистина търсим тук, е неизменността. Искаме състоянието на нашата маса да действа „по-скоро като филм, отколкото като отделна картина“. Което означава, че не искаме да променяме свойствата на състоянието на място, за да актуализираме изгледа, искаме да го заменим изцяло. По този начин всяка промяна в състоянието ще накара всички наши didReceiveAttrs кукички и изчислени свойства да се актуализират, без да се налага да слушаме всяко отделно свойство в обекта.

Microstates ви позволява да създавате обекти на състояние, които са САМО неизменни. Не можете да ги промените, без да генерирате чисто нов.

Така че, ако имате това:

Това ще създаде нов обект на състояние Table, който е сортиран във възходящ ред. Ако искаме да променим сортирането в низходящ ред, можем да направим следното:

Сега в този момент може да очаквате променливата table да бъде сортирана desc сега. Но ще сгрешите. Микросъстоянията са неизменни, което означава, че извършването на промяна няма ефект върху оригинала. Вместо това трябва да направите нещо подобно:

Въпреки че това е различно от това, с което сме свикнали, има някои наистина страхотни печалби.

Нещата остават синхронизирани

Тъй като всяка промяна в нашето дърво на състоянието на таблицата създава изцяло нов обект, сега можем да сме сигурни, че нашите didReceiveAttrs кукички и изчислени свойства ще се актуализират при всяка промяна. Вече не трябва да указваме всеки ключ, който искаме да наблюдаваме, вместо това можем просто да укажем свойството tableState.

Принудително изпълнение на ДДАУ

В Emberland™️ ние сме привърженици на DDAU и открихме, че това е полезен модел, но самата рамка не прави много, за да ни принуди да го направим. В предишните ми примери беше много лесно да извикам set(this.sorts, ‘dir’, ‘desc’) и да променям подаден обект, без да използвам действие.

С помощта на Microstate можете да наложите DDAU във вашите компоненти. Тъй като всеки метод за преход на микросъстояние връща напълно нов обект, дъщерният компонент не може да мутира оригиналния обект, предаден му от родител. Следователно е принуден да използва действие, ако иска да актуализира данните.

Компромисът с DDAU е, че понякога може да направи поддръжката по-трудна, когато трябва да предадем както данните, така и действие за актуализиране на тези данни по целия път през йерархията на нашите компоненти. Обектите с микросъстояние имат „методи за преход“ върху тях, които са специфични за типа (Boolean има toggle, Array има push и т.н.), така че защо трябва да се нуждаем от допълнителни разходи за предаване на допълнително свойство само за актуализиране на родител?

За щастие, „поддръжниците“ на Microstates.js са толкова внимателни и са създали начин да отменят този компромис.

Неизменно състояние с променлив синтаксис

Вместо да се налага да изпълняваме танца на извикване на метод за преход, предаване на новогенерирания обект до действие и след това обратно предаване, Microstates.js ни предоставя функция Store. Това не е Store като EmberData store, въпреки че името го кара да звучи така. Вместо това това е функция, която взема обект на микросъстояние и след това извиква обратно извикване по всяко време, когато обектът премине към ново състояние.

Това означава, че вместо да се налага да предавате всеки нов обект на микросъстояние чрез действие, вие автоматично ще предавате новото си състояние на вашите компоненти, когато се случи преход.

Store API е малко на ниско ниво и би било досадно да се използва през цялото време. За щастие обвивката на ember-addon на Microstates.js ни предоставя 2 API от по-високо ниво за създаване на микросъстояния по този начин:

  • Помощникът {{state}} за шаблони
  • Макросът за изчислено свойство state() за JS

Държавният помощник

Помощникът {{state}} може да се използва сам или заедно с помощника {{type}}, който добавката също предоставя. Помощникът {{type}} ще ви позволи да използвате персонализирани класове Microstate, които сте дефинирали, и очаква те да живеят в app/types.

Макросът за изчислена собственост на състоянието

Макросът за изчислено свойство state е полезен, когато имате нужда от създаване на микросъстояние в JS файл. Той очаква вече създадено микросъстояние да бъде предадено към него и след това се грижи да се актуализира при всеки преход.

Едно предизвикателство, което трябва да имате предвид при макроса, е, че не можете да препращате към други свойства във вашия компонент, за да създадете стойността на първоначалното състояние. Ще трябва да създадете това извън дефиницията на макроса и да използвате метод на преход, за да актуализирате микросъстоянието, което сте създали с макроса.

Така например, това няма да работи:

Вместо това ще трябва да направите нещо по-подобно на това:

Много по-малко шаблонно

Когато използвате макроса Store, {{state}} помощник или state, получавате някои добри предимства. Единият е, че не е нужно да обсипвате приложението си с действия всеки път, когато искате да актуализирате просто свойство. Store поставя затваряне около вашия обект на състояние, така че можете да извиквате неговите методи за преход директно в шаблона.

Така че вместо да направите това:

Сега можете просто да направите:

Заключение

Настройването на структура от данни с разумен размер за задържане на състоянието на нашите таблици беше малко болезнено веднага. Сблъскахме се с някои проблеми:

  • Списъкът с аргументи на нашия компонент стана голям, тъй като трябваше да предаваме както действия, така и свойства за всяко състояние, от което се нуждаехме
  • Поставянето на цялото състояние в един обект или някои по-малки обекти също беше трудно, защото A. Ember няма конвенция за това къде да постави този тип обекти и B. нашите изчислени свойства и didReceiveAttrs куки станаха трудни за поддръжка.
  • За дъщерните компоненти стана лесно да променят състояние, което не притежават, и да създават объркващи ситуации.

Микродържавите решиха тези проблеми вместо нас:

  • Те ни дадоха начин да създадем неизменна структура от данни, която да актуализира нашите изчислени свойства при всяка промяна и не може да бъде мутирана от дъщерни компоненти
  • Те ни дадоха специално място за поставяне на нашите обекти на състоянието в директорията types
  • Те ни дадоха помощници и макроси, които автоматично актуализираха свойствата на нашите компоненти, когато нашето състояние се промени и по този начин елиминираха необходимостта от предаване на действие за всяка част от състоянието

Какво следва?

В следващата статия ще говоря за някои съвети и трикове за използване на Microstates във вашето приложение, както и как да избегнете някои „проблеми“.