<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lomray Software LLC</title>
    <description>The latest articles on DEV Community by Lomray Software LLC (lomray-software).</description>
    <link>https://dev.to/lomray-software</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F7322%2F39bc93b6-7d09-4928-9b50-922580b5b753.jpeg</url>
      <title>DEV Community: Lomray Software LLC</title>
      <link>https://dev.to/lomray-software</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lomray-software"/>
    <language>en</language>
    <item>
      <title>react-mobx-manager 4.6.0: persisting MobX stores without rewriting all of them</title>
      <dc:creator>Melissa Ashford</dc:creator>
      <pubDate>Thu, 05 Nov 2026 21:50:25 +0000</pubDate>
      <link>https://dev.to/lomray-software/react-mobx-manager-460-persisting-mobx-stores-without-rewriting-all-of-them-2fb7</link>
      <guid>https://dev.to/lomray-software/react-mobx-manager-460-persisting-mobx-stores-without-rewriting-all-of-them-2fb7</guid>
      <description>&lt;p&gt;Persistence in react-mobx-manager used to be simple and a bit wasteful. Every persisted store had a MobX reaction on &lt;code&gt;toJS(store)&lt;/code&gt;, and &lt;code&gt;toJS&lt;/code&gt; walks every observable the store can reach, including other stores referenced from its fields. So a loading flag flipping in one store could make another store serialize itself again. Then every save wrote the whole storage value: one JSON blob holding all persisted stores.&lt;/p&gt;

&lt;p&gt;Version 4.6.0 (released 30 September, PR #53, promoted to latest in #54) reworks this. Short version: it saves less, and less often.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the listener watches now
&lt;/h2&gt;

&lt;p&gt;The change listener tracks only the fields that are actually persisted. That is the union of the &lt;code&gt;attributes&lt;/code&gt; you route to storages, or all exported fields when you use &lt;code&gt;'*'&lt;/code&gt;. Stores referenced from those fields are no longer traversed.&lt;/p&gt;

&lt;p&gt;So if a persisted store also has short-lived fields, keep them out of storage. They stop causing persist work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;makeExported&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@lomray/react-mobx-manager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persistStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FeedStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;feed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;behaviour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// or inside the store constructor&lt;/span&gt;
&lt;span class="nf"&gt;makeExported&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;isFetching&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;excluded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Saves are batched
&lt;/h2&gt;

&lt;p&gt;Stores that change within &lt;code&gt;options.persistDelay&lt;/code&gt; (100 ms by default) are serialized once, and each storage gets one write. A store whose persisted state did not change is not written again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7dhw2nlrhcnuke147d3l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7dhw2nlrhcnuke147d3l.png" alt="Diagram of how a change gets saved: before, a reaction on toJS walks referenced stores and rewrites one JSON blob right away; now the listener watches only persisted fields, batches saves for persistDelay and writes once per storage" width="799" height="462"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;persistDelay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a behaviour change. A save can now land up to &lt;code&gt;persistDelay&lt;/code&gt; after the change. Need the old timing? Set &lt;code&gt;persistDelay: 0&lt;/code&gt;. On the server the manager always saves immediately.&lt;/p&gt;

&lt;p&gt;The queue is written right away in four cases. The page is hidden (&lt;code&gt;pagehide&lt;/code&gt;, or &lt;code&gt;visibilitychange&lt;/code&gt; to hidden). A store with queued changes is destroyed. You call &lt;code&gt;manager.destroy()&lt;/code&gt;. Or you call &lt;code&gt;manager.flushPersist()&lt;/code&gt; yourself. React Native has no page events, so flush when the app goes to background:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flushPersist&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;flushPersist()&lt;/code&gt; resolves when the writes finish, to &lt;code&gt;false&lt;/code&gt; if something failed or persistence is disabled. In tests that read storage right after a change, await it first.&lt;/p&gt;

&lt;h2&gt;
  
  
  One key per store: KeyValueStorage
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LocalStorage&lt;/code&gt;, &lt;code&gt;SessionStorage&lt;/code&gt;, &lt;code&gt;CookieStorage&lt;/code&gt; and &lt;code&gt;AsyncStorage&lt;/code&gt; keep all persisted stores in one value, and their data format did not change in 4.6.0. If you have many or large stores, &lt;code&gt;KeyValueStorage&lt;/code&gt; saves each store under its own key and writes only the stores that changed. Values also stay small. The docs point out why that matters on Android: a single AsyncStorage row of around 2 MB may fail to load.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-native-async-storage/async-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Manager&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@lomray/react-mobx-manager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MobxAsyncStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@lomray/react-mobx-manager/storages/async-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CombinedStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@lomray/react-mobx-manager/storages/combined-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;KeyValueStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@lomray/react-mobx-manager/storages/key-value-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CombinedStorage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;KeyValueStorage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// stores saved by the previous setup are moved on the first start&lt;/span&gt;
      &lt;span class="na"&gt;migrateFrom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MobxAsyncStorage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any backend with &lt;code&gt;getItem&lt;/code&gt;, &lt;code&gt;setItem&lt;/code&gt; and &lt;code&gt;removeItem&lt;/code&gt; works, sync or async. MMKV, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMMKV&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-mmkv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mmkv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMMKV&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;KeyValueStorage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mmkv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mmkv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mmkv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stores land under &lt;code&gt;stores:&amp;lt;storeId&amp;gt;&lt;/code&gt; next to a &lt;code&gt;stores-keys&lt;/code&gt; index. &lt;code&gt;prefix&lt;/code&gt; changes the prefix.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe5xajdgk738yfo3webuw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe5xajdgk738yfo3webuw.png" alt="Diagram comparing one storage value that is rewritten on every change with KeyValueStorage, where only the changed store key is written" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration is conservative
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;migrateFrom&lt;/code&gt; is read only when the new storage has no index yet, which means the first start. The old data stays where it was by default, so rolling back to an older app version still finds it. Pass &lt;code&gt;shouldRemoveMigrated: true&lt;/code&gt; to remove it right after migration. If the migrated stores cannot be saved, the app still starts with the old data, and every store is saved again on the next change.&lt;/p&gt;

&lt;h2&gt;
  
  
  IndexedDB for the web
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;IndexedDBStorage&lt;/code&gt; saves each store as its own record. Writes are asynchronous, and the quota is much larger than the roughly 5 MB localStorage gives you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CombinedStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@lomray/react-mobx-manager/storages/combined-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;IndexedDBStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@lomray/react-mobx-manager/storages/indexed-db-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;LocalStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@lomray/react-mobx-manager/storages/local-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CombinedStorage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IndexedDBStorage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// moved on the first start and used when IndexedDB is not available&lt;/span&gt;
    &lt;span class="na"&gt;migrateFrom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LocalStorage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State comes back asynchronously in &lt;code&gt;manager.init()&lt;/code&gt;. Wait for it before rendering persisted stores, for example with &lt;code&gt;shouldInit&lt;/code&gt; on &lt;code&gt;StoreManagerProvider&lt;/code&gt;. If IndexedDB cannot be opened, the storage reads and writes &lt;code&gt;migrateFrom&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller things
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Custom storages can implement an optional &lt;code&gt;saveChanges({ value, changes, toJSON })&lt;/code&gt; to write only what changed. Plain &lt;code&gt;set&lt;/code&gt; still works.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CookieStorage&lt;/code&gt; warns once in the console when the value goes over 4 KB.&lt;/li&gt;
&lt;li&gt;A store that cannot be serialized (a BigInt, say) no longer breaks saving for the others.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Numbers, with a caveat
&lt;/h2&gt;

&lt;p&gt;PR #53 includes a synthetic benchmark: 24 persisted stores, about 750 KB of state, stores referencing other stores, frequent loading-flag changes, an AsyncStorage-like backend, run in Node in client mode.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after, single value&lt;/th&gt;
&lt;th&gt;after, KeyValueStorage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU spent on persistence&lt;/td&gt;
&lt;td&gt;~3.8 s&lt;/td&gt;
&lt;td&gt;~0.1-0.2 s&lt;/td&gt;
&lt;td&gt;~0.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;storage writes&lt;/td&gt;
&lt;td&gt;92&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;71 (small)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bytes written&lt;/td&gt;
&lt;td&gt;47 MB&lt;/td&gt;
&lt;td&gt;7.6 MB&lt;/td&gt;
&lt;td&gt;1.4 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The final persisted state matched the previous version. It is one synthetic setup, and your app will show different numbers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdiotm3likwuimj7z6e9p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdiotm3likwuimj7z6e9p.png" alt="Bar charts from the PR benchmark: bytes written, storage writes and CPU time on persistence, before and after the change, with and without KeyValueStorage" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Release v4.6.0: &lt;a href="https://github.com/Lomray-Software/react-mobx-manager/releases/tag/v4.6.0" rel="noopener noreferrer"&gt;https://github.com/Lomray-Software/react-mobx-manager/releases/tag/v4.6.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #53 with the full change list: &lt;a href="https://github.com/Lomray-Software/react-mobx-manager/pull/53" rel="noopener noreferrer"&gt;https://github.com/Lomray-Software/react-mobx-manager/pull/53&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Persistence recipes: &lt;a href="https://lomray-software.github.io/react-mobx-manager/examples/recipes" rel="noopener noreferrer"&gt;https://lomray-software.github.io/react-mobx-manager/examples/recipes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Manager API: &lt;a href="https://lomray-software.github.io/react-mobx-manager/api/manager" rel="noopener noreferrer"&gt;https://lomray-software.github.io/react-mobx-manager/api/manager&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tests for 4.6.0 pass on MobX 6.16 and 7.0. Install: &lt;code&gt;npm i @lomray/react-mobx-manager @lomray/consistent-suspense&lt;/code&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>mobx</category>
      <category>reactnative</category>
      <category>javascript</category>
    </item>
    <item>
      <title>A request for /.env shouldn't render your React app</title>
      <dc:creator>Melissa Ashford</dc:creator>
      <pubDate>Tue, 22 Sep 2026 01:17:49 +0000</pubDate>
      <link>https://dev.to/lomray-software/a-request-for-env-shouldnt-render-your-react-app-4j0p</link>
      <guid>https://dev.to/lomray-software/a-request-for-env-shouldnt-render-your-react-app-4j0p</guid>
      <description>&lt;p&gt;A request for &lt;code&gt;/.env&lt;/code&gt; doesn't need a React tree. In the latest release, vite-ssr-boost can reject it before your request hook runs.&lt;/p&gt;

&lt;p&gt;The document guard is on by default. The SSR concurrency limit isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your hook might not run
&lt;/h2&gt;

&lt;p&gt;The default document methods are GET, HEAD and POST. Other methods get 405 with an &lt;code&gt;Allow&lt;/code&gt; header before &lt;code&gt;onRequest&lt;/code&gt;, HTML loading or route loaders. If your hook handles CORS preflights, include OPTIONS in &lt;code&gt;requestGuard.methods&lt;/code&gt; alongside the methods you still need. That array replaces the defaults.&lt;/p&gt;

&lt;p&gt;Allowing a method doesn't bypass the other checks. For allowed methods, oversized targets get 414 and malformed paths get 400. With the default rules, GET requests for &lt;code&gt;/.env&lt;/code&gt; or &lt;code&gt;/random.php&lt;/code&gt; get a plain 404 without rendering. An unmatched &lt;code&gt;/missing.xml&lt;/code&gt; does too; a matched resource route such as &lt;code&gt;/sitemap.xml&lt;/code&gt; can pass.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;requestGuard: false&lt;/code&gt; disables the new guard and its missing-page handling. This is document-handler behavior, not protection for every request reaching your server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose what a missing page does
&lt;/h2&gt;

&lt;p&gt;For an unmatched document such as &lt;code&gt;/missing&lt;/code&gt;, &lt;code&gt;notFound&lt;/code&gt; defaults to &lt;code&gt;render&lt;/code&gt;: the existing router/render path. &lt;code&gt;spa&lt;/code&gt; serves a client shell with 404; detected bots still take the render path under the default bot policy. A custom Response gives you a static 404 without the render pipeline.&lt;/p&gt;

&lt;p&gt;A catch-all route counts as a match. To apply a missing-page mode there, return &lt;code&gt;'notFound'&lt;/code&gt; from &lt;code&gt;requestGuard.decide&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cached&lt;/code&gt; buffers a router 404 and reuses it while the entry is retained. Concurrent misses for the same key share a render. Hits skip &lt;code&gt;onRequest&lt;/code&gt; and the render pipeline, including loaders and admission. Don't depend on those hooks running for every missing page.&lt;/p&gt;

&lt;p&gt;The default key is shared across missing paths, including the first rendered URL and hydration data. Cold renders use GET without the original body, with Cookie and Authorization removed before the request hook. The URL, other headers and application state can still affect the result. Keep private state out of shared HTML; choose keys for public variations such as locale. If the page depends on a session, keep ordinary rendering instead.&lt;/p&gt;

&lt;p&gt;A configured CSP nonce disables this cache and falls back to ordinary rendering. Failed renders and non-404 results aren't retained. Overloads and server failures keep their error status; a request-hook bypass keeps its own response. The default &lt;code&gt;private, no-store&lt;/code&gt; header on 404 documents can be overridden by document header rules. This HTML cache is separate from browser or CDN caching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limit renders, not every bit of server work
&lt;/h2&gt;

&lt;p&gt;Admission is opt-in and handler-local, not a cluster-wide cap.&lt;/p&gt;

&lt;p&gt;Set a positive safe integer in &lt;code&gt;admission.maxConcurrency&lt;/code&gt;, or a valid &lt;code&gt;SSR_MAX_CONCURRENCY&lt;/code&gt;. The environment value wins and is read at handler/entry creation. No limit is enabled by default.&lt;/p&gt;

&lt;p&gt;A slot is taken after request initialization and the SSR/SPA decision, before route loaders. At capacity the default response is 503 with &lt;code&gt;Retry-After&lt;/code&gt; and &lt;code&gt;private, no-store&lt;/code&gt;. There is no admission queue. The rejected request's loaders don't run, but &lt;code&gt;onRequest&lt;/code&gt; and HTML loading have already happened.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;admission.overload: 'spa'&lt;/code&gt;, requests that reach a full admission controller get a 200 shell for humans and 503 for detected bots. That isn't the missing-page SPA mode, whose shell uses 404. A custom overload Response is sent as 503.&lt;/p&gt;

&lt;p&gt;On normal streamed completion, the slot stays occupied until the final Fetch response stream is consumed, not merely until React produces a shell. This doesn't mean the last byte has reached the browser. Aborts and errors can release it earlier; redirects and bodyless responses have their own release paths. Ordinary SPA shells and cached 404 hits don't occupy a slot.&lt;/p&gt;

&lt;p&gt;Before deploying, test a preflight that must reach your hook. Check that missing URLs don't share private data. For admission, keep one response stream open and send another SSR request at capacity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lomray-software.github.io/vite-ssr-boost/api/request-guard" rel="noopener noreferrer"&gt;Configuration reference&lt;/a&gt;, &lt;a href="https://github.com/Lomray-Software/vite-ssr-boost/blob/db2b419e16bee8dfdc5c31577af1d21ada833cf5/__tests__/core/not-found.ts" rel="noopener noreferrer"&gt;guard and 404 tests&lt;/a&gt;, &lt;a href="https://github.com/Lomray-Software/vite-ssr-boost/blob/db2b419e16bee8dfdc5c31577af1d21ada833cf5/__tests__/core/admission.ts" rel="noopener noreferrer"&gt;admission tests&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>We deleted Vite from our production server</title>
      <dc:creator>Melissa Ashford</dc:creator>
      <pubDate>Mon, 21 Sep 2026 16:59:28 +0000</pubDate>
      <link>https://dev.to/lomray-software/we-deleted-vite-from-our-production-server-4n1l</link>
      <guid>https://dev.to/lomray-software/we-deleted-vite-from-our-production-server-4n1l</guid>
      <description>&lt;p&gt;vite-ssr-boost is an SSR framework built on Vite. Its production server used to start this way: load Vite, resolve vite.config, build the full CLI command tree, then serve the first request. None of that is needed to serve HTML.&lt;/p&gt;

&lt;p&gt;In 8.4.x we cut it out. The numbers below are our own runs on a pinned template, M3 Pro, installed through npm.&lt;/p&gt;

&lt;p&gt;What the production start used to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;load Vite and evaluate vite.config&lt;/li&gt;
&lt;li&gt;build the whole Commander command tree just to parse the flags of &lt;code&gt;start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;spin up the source analysis services that only dev mode uses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ssr-boost build&lt;/code&gt; writes a versioned &lt;code&gt;build/server/ssr-boost.json&lt;/code&gt; with the resolved serving settings. Production reads that file and never evaluates the Vite config.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ssr-boost start&lt;/code&gt; parses its flags with Node's own parser. Every other command loads on demand, keyboard shortcuts load only when there is a TTY.&lt;/li&gt;
&lt;li&gt;An import hook watches the serving path. It rejects vite, rolldown, rollup, commander, diff, parse5, semver, json5 and the babel packages, and it also rejects our own dev-only code: the cli commands, the build, manifest and route parsing services, the plugins. That is the part that keeps it honest. The hook catches accidental imports before they reach production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it bought:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cold start 354 ms to 249 ms. A plain Express server with renderToPipeableStream is 171 ms on the same machine.&lt;/li&gt;
&lt;li&gt;RSS 142 MiB to 100 MiB. Same plain Express baseline: 69 MiB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the per-request path. React's native Web stream on React 19.2+, the production shell cached instead of rebuilt, boundary scanning only while data frames can still arrive, a plain state script for routes without router data, static dispatch by built prefixes so there is no filesystem lookup per HTML request, the header queued at shell readiness, compression flushed before drain.&lt;/p&gt;

&lt;p&gt;One more part, the one I would copy even if you skip everything else. Acceptance measures a plain Express baseline in the same run and fails the build if cold start goes past 2x that baseline. RSS has its own ceiling in the same file. So if a later change drags the start path back toward Vite, CI fails on a number instead of on someone's memory.&lt;/p&gt;

&lt;p&gt;We also keep a public benchmark repo that runs five SSR frameworks weekly in CI and stores the raw json for every cell: &lt;a href="https://github.com/Lomray-Software/ssr-benchmarks" rel="noopener noreferrer"&gt;https://github.com/Lomray-Software/ssr-benchmarks&lt;/a&gt;. Read it yourself, our claims here are about our own before and after, not about anyone else's framework.&lt;/p&gt;

&lt;p&gt;If you run SSR on Vite, go look at what your production process loads before it answers anything. Count the modules, or run start under --cpu-prof.&lt;/p&gt;

&lt;p&gt;We were loading a build tool in production. It cost about 100 ms and 40 MiB per process.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/Lomray-Software/vite-ssr-boost" rel="noopener noreferrer"&gt;https://github.com/Lomray-Software/vite-ssr-boost&lt;/a&gt;&lt;br&gt;
Docs: &lt;a href="https://lomray-software.github.io/vite-ssr-boost/" rel="noopener noreferrer"&gt;https://lomray-software.github.io/vite-ssr-boost/&lt;/a&gt;&lt;br&gt;
The PRs with the full detail: &lt;a href="https://github.com/Lomray-Software/vite-ssr-boost/pull/53" rel="noopener noreferrer"&gt;https://github.com/Lomray-Software/vite-ssr-boost/pull/53&lt;/a&gt;, &lt;a href="https://github.com/Lomray-Software/vite-ssr-boost/pull/54" rel="noopener noreferrer"&gt;https://github.com/Lomray-Software/vite-ssr-boost/pull/54&lt;/a&gt;, &lt;a href="https://github.com/Lomray-Software/vite-ssr-boost/pull/55" rel="noopener noreferrer"&gt;https://github.com/Lomray-Software/vite-ssr-boost/pull/55&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Creating Mobx Stores Manager: A Fresh Perspective</title>
      <dc:creator>Mikhail Yarmaliuk</dc:creator>
      <pubDate>Fri, 04 Aug 2023 10:42:29 +0000</pubDate>
      <link>https://dev.to/lomray-software/creating-mobx-stores-manager-a-fresh-perspective-2jb6</link>
      <guid>https://dev.to/lomray-software/creating-mobx-stores-manager-a-fresh-perspective-2jb6</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flc9ck4xgaaa2clqg3ctc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flc9ck4xgaaa2clqg3ctc.png" alt="Mobx store manager logo" width="300" height="303"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Manage your Mobx stores like a boss — debug like a hacker.
  &lt;br&gt;
&lt;p&gt;&lt;/p&gt;




&lt;p&gt;Hello there!👋 I'm Mikhail, the Chief Technology Officer👮‍♀️ at Lomray Software. Today, I'm excited to share our decision in mobx for managing stores.&lt;/p&gt;

&lt;p&gt;In a sea🌊 of existing solutions, you might be wondering if there's room for one more. Allow me to assure you — this isn't just another rehash of the same old state tree concept. While established approaches certainly have their merits, I propose an alternative. As the saying goes, "two heads are better than one" and in that spirit, I present to you an innovative perspective that I believe everyone should consider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A New Take on State Management:&lt;/strong&gt;&lt;br&gt;
If you're anything like me, you've grown somewhat weary of the conventional state tree🌲 models. Whether it's the state tree of Redux or the Mobx approach, it's hard to escape their omnipresence, even in basic Google searches. Seeking something both novel and familiar, I embarked on a journey🔍 to reimagine the paradigm, particularly after transitioning from Redux to Mobx not long ago.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { combineReducers } from 'redux';
import AppInfoReducer from '@store/modules/app-info/reducer';
import FormApplicationReducer from '@store/modules/form-application/reducer';
import FormPopupReducer from '@store/modules/form-popup/reducer';
import FormQuizReducer from '@store/modules/form-quiz/reducer';
import FormSubscribeReducer from '@store/modules/form-subscribe/reducer';
import PageMetaReducer from '@store/modules/page-meta/reducer';
import BlogDetailsReducer from '@store/modules/pages/blog-details/reducer';
import BlogInfoReducer from '@store/modules/pages/blog/info/reducer';
import BlogListReducer from '@store/modules/pages/blog/list/reducer';
import BlogTagsReducer from '@store/modules/pages/blog/tags/reducer';
import Think3DetailsReducer from '@store/modules/pages/think3-details/reducer';
import Think3CitiesReducer from '@store/modules/pages/think3/cities/reducer';
import Think3InfoReducer from '@store/modules/pages/think3/info/reducer';
import Think3ListReducer from '@store/modules/pages/think3/list/reducer';
import Think3TagsReducer from '@store/modules/pages/think3/tags/reducer';
import Think1DetailsReducer from '@store/modules/pages/think1-details/reducer';
import Think1InfoReducer from '@store/modules/pages/think1/info/reducer';
import Think1ListReducer from '@store/modules/pages/think1/list/reducer';
import Think1TagsReducer from '@store/modules/pages/think1/tags/reducer';
import ContactPageReducer from '@store/modules/pages/contact/reducer';
import HomeGraphReducer from '@store/modules/pages/home/graph/reducer';
import HomePageReducer from '@store/modules/pages/home/info/reducer';
import Think2DetailsReducer from '@store/modules/pages/think2-details/reducer';
import Think2InfoReducer from '@store/modules/pages/think2/info/reducer';
import Think2ListReducer from '@store/modules/pages/think2/list/reducer';
import Think2TagsReducer from '@store/modules/pages/think2/tags/reducer';
import OurTeamPageReducer from '@store/modules/pages/our-team/reducer';
import PrivacyPolicyReducer from '@store/modules/pages/privacy-policy/reducer';
import SearchReducer from '@store/modules/pages/search/reducer';
import FormCvReducer from '@store/modules/post-cv/reducer';

const reducers = {
  pages: combineReducers({
    home: combineReducers({
      info: HomePageReducer,
      graph: HomeGraphReducer,
    }),
    think1: combineReducers({
      info: Think1InfoReducer,
      list: Think1ListReducer,
      tags: Think1TagsReducer,
    }),
    think1Details: Think1DetailsReducer,
    think2: combineReducers({
      info: Think2InfoReducer,
      list: Think2ListReducer,
      tags: Think2TagsReducer,
    }),
    think2Details: Think2DetailsReducer,
    ourTeam: OurTeamPageReducer,
    think3: combineReducers({
      info: Think3InfoReducer,
      tags: Think3TagsReducer,
      list: Think3ListReducer,
      cities: Think3CitiesReducer,
    }),
    think3Details: Think3DetailsReducer,
    blog: combineReducers({
      info: BlogInfoReducer,
      list: BlogListReducer,
      tags: BlogTagsReducer,
    }),
    blogDetails: BlogDetailsReducer,
    contact: ContactPageReducer,
    search: SearchReducer,
    privacyPolicy: PrivacyPolicyReducer,
  }),
  appInfo: AppInfoReducer,
  formSubscribe: FormSubscribeReducer,
  formCv: FormCvReducer,
  pageMeta: PageMetaReducer,
  formPopup: FormPopupReducer,
  formApplication: FormApplicationReducer,
  formQuiz: FormQuizReducer,
};

export default reducers;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
It’s small application😰 (some names replaced)





&lt;p&gt;&lt;strong&gt;A Challenge of Duplication:&lt;/strong&gt;&lt;br&gt;
Let's be candid—have you ever sought a solution that facilitates the concurrent use of components connected to a store? Have you faced the task of running a connected component multiple times across various pages📑 in your React app or screens in React Native? As you survey your app, you might find the state tree🌴🌴🌴 growing unwieldy, leading to complex code (as demonstrated above). While alternate libraries could potentially address this, I wanted a different solution—one that aligned with the class-based approach that Mobx employs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faywa8mhb95q2wkhd1mjj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faywa8mhb95q2wkhd1mjj.png" alt="State tree problem image" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;
Try to run same page twice, which connected to one state tree node



&lt;p&gt;&lt;strong&gt;Embracing Mobx's Strengths:&lt;/strong&gt;&lt;br&gt;
I've always been drawn✍️ to Mobx's class-based methodology. The advantages offered by classes are difficult to ignore, especially when it comes to business logic and state management. However, a curious cycle emerges—while attempting to streamline one library's functionality, we often find ourselves introducing another, and then another. This chain of dependencies complicates development, requiring mastery of multiple tools and their nuances. Furthermore, onboarding new developers to this "monster"🐙 can be a daunting task.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5yn839ifcalwc3cdim9j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5yn839ifcalwc3cdim9j.png" alt="Proble with documentation image" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;
Try to write app with new state management library




&lt;p&gt;&lt;strong&gt;The Quest for Centralization:&lt;/strong&gt;&lt;br&gt;
Although Mobx's documentation showcases rudimentary store usage examples, it becomes apparent that a centralized approach is missing. Interactions between stores and certain debugging tools may feel reminiscent of Redux—an acquaintance many of us share. This lack of seamless integration and default code splitting options is evident upon deeper🪔 exploration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
 * Example redux
 */

/**
 * Action types
 */
enum EXAMPLE_ACTION_TYPE {
  EXAMPLE_USER_GET = 'EXAMPLE_USER_GET',
  EXAMPLE_USER_GET_SUCCESS = 'EXAMPLE_USER_GET_SUCCESS',
  EXAMPLE_USER_GET_ERROR = 'EXAMPLE_USER_GET_ERROR',
}

/**
 * Action creators
 */

const getUser = (): IAction&amp;lt;EXAMPLE_ACTION_TYPE&amp;gt; =&amp;gt; ({
  type: EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET,
  payload: {},
});

const getUserSuccess = (user: Record&amp;lt;string, any&amp;gt;): IAction&amp;lt;EXAMPLE_ACTION_TYPE&amp;gt; =&amp;gt; ({
  type: EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET_SUCCESS,
  payload: { user },
});

const getUserError = (message: string): IAction&amp;lt;EXAMPLE_ACTION_TYPE&amp;gt; =&amp;gt; ({
  type: EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET_ERROR,
  payload: { message },
});

/**
 * Reducer
 */

const initState = {
  fetching: false,
  error: null,
  result: null,
};

const reducer = (
  state = initState,
  { type, payload }: IAction&amp;lt;EXAMPLE_ACTION_TYPE&amp;gt; = {},
): IAppInfoState =&amp;gt; {
  switch (type) {
    case EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET:
      return { ...initState, fetching: true };

    case EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET_ERROR:
      return { ...initState, error: payload.message };

    case EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET_SUCCESS:
      return { ...initState, result: payload.user };

    default:
      return { ...state };
  }
};

/**
 * Saga
 */

function* getUserSaga(): SagaIterator {
  try {
    // axios request
    const { data } = yield call(() =&amp;gt; axios.get('/users/123'));

    yield put(getUserSuccess(data));
  } catch (e) {
    yield put(getUserError(e.message));
  }
}

export default [
  takeLatest(EXAMPLE_ACTION_TYPE.EXAMPLE_USER_GET, getUserSaga),
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
 * Example Mobx state tree
 */

import { values } from "mobx"
import { types, getParent, flow } from "mobx-state-tree"

export const User = types.model("User", {
  id: types.identifier,
  name: types.string,
})

export const UserStore = types
  .model("UserStore", {
    fetching: false,
    error: null,
    user: types.reference(User),
  })
  .actions((self) =&amp;gt; {
    function setIsFetching(fetching) {
      self.fetching = fetching
    }

    function setUser(user) {
      self.user = user;
    }

    function setError(error) {
      self.error = error;
    }

    function getUser() {
      try {
        setIsFetching(true);
        const { data } = await axios.get("/users/123")
        setUser(data);
        setIsFetching(false)
      } catch (err) {
        setError("Failed to load user")
      }
    })

    return {
      getUser,
      setUser
    }
  })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { makeObservable, observable, action } from "mobx"

interface IUser {
  id: number;
  name: string;
}

class UserStore {
  user: IUser | null = null;
  fetching = false;
  error: string | null = null;

  constructor() {
    makeObservable(this, {
      user: observable,
      fetching: observable,
      error: observable,
      setIsFetching: action.bound,
      setError: action.bound,
      setUser: action.bound,
    })
  }

  setIsFetching(fetching: boolean) {
    this.fetching = fetching;
  }

  setError(error: string | null) {
    this.error = error;
  }

  setUser(user) {
    this.user = user;
  }

  getUser = async () =&amp;gt; {
    this.setIsFetching(true);

    try {
      const {data} = await axios.get("/users/123")
      this.setUser(data);
    } catch (e) {
      this.setError("Failed to load user")
    } finally {
      this.setIsFetching(false);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Introducing React Mobx Manager:&lt;/strong&gt;&lt;br&gt;
Enter the solution: &lt;a href="https://github.com/Lomray-Software/react-mobx-manager" rel="noopener noreferrer"&gt;React Mobx Manager&lt;/a&gt;. Picture this as a singleton—an information repository housing details about all instantiated stores, their contexts, and the linked component names. With this approach, you sidestep the need to build and configure an entire state tree from scratch. Instead, you create dedicated stores for each component, only as necessary. Furthermore, accessing a pre-existing store from another store becomes a breeze🌬💧. The need for complex code splitting is diminished through clever store techniques that mimic React context functionality. You can even concurrently mount identical pages or screens while retaining state integrity. For those interested, a Reactotron plugin is available, and work on browser🌐 extensions is underway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visualizing the Concept:&lt;/strong&gt;&lt;br&gt;
To illustrate this concept visually, consider the following diagram:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1jzfb76d9srh3iyq1f1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1jzfb76d9srh3iyq1f1.png" alt="How Mobx Manager works image" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;
Mobx Store Manager idea




&lt;p&gt;First we need to create a manager and wrap our application in a context (entrypoint src/index.tsx)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import React from 'react';
import ReactDOM from 'react-dom/client';
import { Manager, StoreManagerProvider } from '@lomray/react-mobx-manager';
import User from './user';

const storeManager = new Manager();

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(
  &amp;lt;React.StrictMode&amp;gt;
    &amp;lt;StoreManagerProvider storeManager={storeManager} shouldInit fallback={&amp;lt;div&amp;gt;Loading…&amp;lt;/div&amp;gt;}&amp;gt;
      &amp;lt;User /&amp;gt;
    &amp;lt;/StoreManagerProvider&amp;gt;
  &amp;lt;/React.StrictMode&amp;gt;,
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we create a store and connect it to the component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import type { FC } from 'react';
import { makeObservable, observable } from 'mobx';
import type { IConstructorParams, StoresType } from '@lomray/react-mobx-manager';
import { withStores } from '@lomray/react-mobx-manager';

class UserStore {
  /**
   * Required only if we don't configure our bundler to keep classnames and function names
   * Default: current class name
   */
  static id = 'user';

  /**
   * You can also enable 'singleton' behavior for global application stores
   * Default: false
   */
  static isSingleton = true;

  /**
   * Our state
   */
  public name = 'Matthew'

  /**
   * @constructor
   */
  constructor(params: IConstructorParams) {
    makeObservable(this, {
     name: observable,
   });
  }
}

/**
 * Define stores for component
 */
const stores = {
 userStore: UserStore
};

/**
 * Support typescript
 */
type TProps = StoresType &amp;lt;typeof stores&amp;gt;;

/**
 * User component
 */
const User: FC&amp;lt;TProps&amp;gt; = ({ userStore: { name } }) =&amp;gt; {
 return (
   &amp;lt;div&amp;gt;{name}&amp;lt;/div&amp;gt;
 )
}

/**
 * Connect stores to component
 */
export default withStores(User, stores);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done!✅&lt;/p&gt;

&lt;p&gt;I’ll just show how it all looks in the reactotron debugger:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzq1kj6r4l3lki4cc4zyl.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzq1kj6r4l3lki4cc4zyl.jpeg" alt="Reactotron mobx manager demo image" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If this has piqued your interest, waste no time in diving🤿 into the mechanics. Allow me to provide you with a practical &lt;a href="https://github.com/Lomray-Software/react-mobx-manager/tree/staging/example" rel="noopener noreferrer"&gt;example&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion:&lt;/strong&gt;&lt;br&gt;
React Mobx Manager isn't gunning for any awards. Rather, it's an alternative perspective and a personal choice. Whether you embrace it or not, the decision ultimately rests with you. I genuinely appreciate your time spent on this article and am eagerly open to any ideas or suggestions you may have on this matter.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Vite SSR BOOST: Forging Our Unique Journey in the Realm of React SSR⚡</title>
      <dc:creator>Mikhail Yarmaliuk</dc:creator>
      <pubDate>Thu, 03 Aug 2023 10:55:42 +0000</pubDate>
      <link>https://dev.to/lomray-software/vite-ssr-boost-forging-our-unique-journey-in-the-realm-of-react-ssr-59hm</link>
      <guid>https://dev.to/lomray-software/vite-ssr-boost-forging-our-unique-journey-in-the-realm-of-react-ssr-59hm</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0mt568ush2zzmozf7hwq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0mt568ush2zzmozf7hwq.png" alt="Vite SSR Boost library logo" width="300" height="303"&gt;&lt;/a&gt;&lt;/p&gt;
Develop ⚡charged⚡ server side applications with Vite SSR Boost






&lt;p&gt;Hello there!👋 I'm Mikhail, the Chief Technology Officer👮‍♀️ at &lt;a href="https://lomray.com" rel="noopener noreferrer"&gt;Lomray Software&lt;/a&gt;. Today, I'm excited to share a brief overview of our journey in creating a custom framework tailored for developing React applications with robust Server-Side Rendering (SSR) support. Additionally, I'll be unveiling the outcomes of our endeavors.&lt;/p&gt;

&lt;p&gt;As the adage goes, monumental achievements often originate from humble beginnings. In our scenario, we initially managed a handful of React applications lacking SSR capabilities. Gradually, the need to transition these applications toward server-side rendering emerged. Given that the standard navigation within regular React applications (SPA, CRA) is managed by the react-router library, we embarked on a quest to find a solution enabling seamless deployment of existing apps with minimal adjustments for SSR. Unfortunately, the landscape for SSR solutions within the React ecosystem, especially in conjunction with react-router🛣, was somewhat limited. Next.js, for instance, didn't align with our objectives due to its routing system and specific features. Eventually, we encountered a framework named after.js, which granted us the means to bring our vision to life. However, over time, we encountered the obstacle of React and related libraries undergoing significant updates while after.js remained stagnant. Despite amassing a commendable number of GitHub stars⭐, its development was unfortunately halted.&lt;/p&gt;

&lt;p&gt;Confronted with a dearth of viable alternatives and armed with insights from current trends, we opted to forge our own framework, leveraging cutting-edge technologies to address prevailing challenges. It's important to acknowledge our appreciation for Next.js and the Vite SSR Plugin, both of which offer an exceptional SSR development experience. Regrettably, their routing systems fell short of our expectations. Our chief aim was to facilitate a seamless transition from a standard React application to SSR (and vice versa), requiring minimal modifications. The ultimate goal was for the resultant application to be deployable in both SSR and SPA modes.&lt;/p&gt;

&lt;p&gt;Allow me to introduce the &lt;a href="https://github.com/Lomray-Software/vite-ssr-boost" rel="noopener noreferrer"&gt;Vite SSR BOOST&lt;/a&gt;🔋. Originally conceptualized as a plugin, it organically matured into a comprehensive library.&lt;/p&gt;

&lt;p&gt;🔑Key highlights of Vite SSR BOOST:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flexibility for developing applications as SSR or SPA (CRA).&lt;/li&gt;
&lt;li&gt;Effortless toggling between SSR and SPA modes at any point.&lt;/li&gt;
&lt;li&gt;Streamlined building process with no need for SSR or SPA-specific adjustments.&lt;/li&gt;
&lt;li&gt;Seamlessly integrated react-router for streamlined navigation.&lt;/li&gt;
&lt;li&gt;Harnessing the full potential of Vite's capabilities.&lt;/li&gt;
&lt;li&gt;Native support for rendering to a stream (rendertopipeablestream).&lt;/li&gt;
&lt;li&gt;Full compatibility with Suspense.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What is notable is that having minimal knowledge in Node.js and React Router is sufficient. Here’s a brief example of an application page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Suspense} from '@lomray/consistent-suspense';
import type { FCRoute } from '@lomray/vite-ssr-boost/interfaces/fc-route';
import { Link, useLoaderData } from 'react-router-dom';
import UserPlaceholder from './components/placeholder';
import User from './components/user';

interface ILoaderData {
  userIds: string[];
}

/**
 * Just simple page which render short info about 3 users by id's
 */
const Details: FCRoute = () =&amp;gt; {
  const { userIds } = useLoaderData() as ILoaderData;

  return (
    &amp;lt;&amp;gt;
      &amp;lt;p&amp;gt;
        Header
      &amp;lt;/p&amp;gt;
      &amp;lt;p&amp;gt;
        Load users for id's: {userIds.join(', ')}
      &amp;lt;/p&amp;gt;
      &amp;lt;div&amp;gt;
        &amp;lt;Suspense fallback={&amp;lt;UserPlaceholder count={2} /&amp;gt;}&amp;gt;
          &amp;lt;&amp;gt;
            &amp;lt;Suspense.NS&amp;gt;
              &amp;lt;User userId="user-1" /&amp;gt;
            &amp;lt;/Suspense.NS&amp;gt;
            &amp;lt;Suspense.NS&amp;gt;
              &amp;lt;User userId="user-3" /&amp;gt;
            &amp;lt;/Suspense.NS&amp;gt;
          &amp;lt;/&amp;gt;
        &amp;lt;/Suspense&amp;gt;
        &amp;lt;Suspense fallback={&amp;lt;UserPlaceholder /&amp;gt;}&amp;gt;
          &amp;lt;User userId="user-2" /&amp;gt;
        &amp;lt;/Suspense&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;p&amp;gt;
        Footer
      &amp;lt;/p&amp;gt;
    &amp;lt;/&amp;gt;
  );
};

/**
 * Just example (react-router API)
 */
Details.loader = (): ILoaderData =&amp;gt; {
  const userIds = ['user-1', 'user-2', 'user-3'];

  return { userIds };
};

export default Details;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certainly, our journey encountered significant challenges along the way. However, in the spirit of the adage, "what doesn't kill you, makes you stronger." Instead of overwhelming you with a multitude of words, I'm thrilled to present a &lt;a href="https://github.com/Lomray-Software/vite-template" rel="noopener noreferrer"&gt;demo repository&lt;/a&gt; showcasing an application where, alongside the framework itself, we've successfully tackled several related development issues.&lt;/p&gt;

&lt;p&gt;We're eager to hear your thoughts and welcome any feedback you may have. Your input serves as a tremendous motivator for us. Thank you for your valuable attention.&lt;/p&gt;

</description>
      <category>react</category>
      <category>vite</category>
      <category>ssr</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
