<?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: Vasyl Kostin</title>
    <description>The latest articles on DEV Community by Vasyl Kostin (@vasyl_kostin_477da9586e09).</description>
    <link>https://dev.to/vasyl_kostin_477da9586e09</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3702382%2F8d039b5d-e505-47fa-a798-2712e6a122a1.jpg</url>
      <title>DEV Community: Vasyl Kostin</title>
      <link>https://dev.to/vasyl_kostin_477da9586e09</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vasyl_kostin_477da9586e09"/>
    <language>en</language>
    <item>
      <title>When client-side entity normalization actually becomes necessary in large React Native apps</title>
      <dc:creator>Vasyl Kostin</dc:creator>
      <pubDate>Fri, 09 Jan 2026 12:12:03 +0000</pubDate>
      <link>https://dev.to/vasyl_kostin_477da9586e09/when-client-side-entity-normalization-actually-becomes-necessary-in-large-react-native-apps-50dg</link>
      <guid>https://dev.to/vasyl_kostin_477da9586e09/when-client-side-entity-normalization-actually-becomes-necessary-in-large-react-native-apps-50dg</guid>
      <description>&lt;p&gt;Over the past few years, while working on several React Native projects,&lt;br&gt;
I kept running into the same type of issues.&lt;/p&gt;

&lt;p&gt;Different products, different teams — but very similar symptoms.&lt;/p&gt;

&lt;p&gt;At first, none of this felt architectural.&lt;br&gt;
Problems were usually solved locally and pragmatically.&lt;br&gt;
Over time, though, a pattern started to repeat itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repeated solutions, repeated problems
&lt;/h2&gt;

&lt;p&gt;As apps grew, the same entities began to appear in more and more places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;feeds&lt;/li&gt;
&lt;li&gt;detail screens&lt;/li&gt;
&lt;li&gt;search results&lt;/li&gt;
&lt;li&gt;notifications&lt;/li&gt;
&lt;li&gt;background updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each new feature introduced small, reasonable decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cache a list response&lt;/li&gt;
&lt;li&gt;refetch on screen focus&lt;/li&gt;
&lt;li&gt;merge partial updates&lt;/li&gt;
&lt;li&gt;add derived selectors&lt;/li&gt;
&lt;li&gt;manually sync data between screens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Individually, these choices made sense.&lt;/p&gt;

&lt;p&gt;Collectively, they were all trying to solve the &lt;strong&gt;same underlying problem&lt;/strong&gt; —&lt;br&gt;
often in slightly different ways and scattered across the codebase.&lt;/p&gt;

&lt;p&gt;At some point, it was clear this wasn’t accidental anymore.&lt;br&gt;
It was structural.&lt;/p&gt;




&lt;h2&gt;
  
  
  When normalization doesn’t matter
&lt;/h2&gt;

&lt;p&gt;For a long time, entity normalization felt unnecessary.&lt;/p&gt;

&lt;p&gt;If data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;belongs to a single screen&lt;/li&gt;
&lt;li&gt;has a short lifecycle&lt;/li&gt;
&lt;li&gt;isn’t reused elsewhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then keeping it close to the API response works perfectly fine.&lt;/p&gt;

&lt;p&gt;In those cases, normalization would mostly add ceremony without much payoff.&lt;/p&gt;

&lt;p&gt;The problem started once data stopped being screen-local.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why popular tools didn’t fully fit this problem
&lt;/h2&gt;

&lt;p&gt;Libraries like Redux Toolkit or React Query are popular for good reasons.&lt;br&gt;
They solve a wide range of problems and do it very well.&lt;/p&gt;

&lt;p&gt;That said, their strength is also their breadth.&lt;br&gt;
They answer many questions at once:&lt;br&gt;
server caching, invalidation, background refetching,&lt;br&gt;
pagination, optimistic updates, and more.&lt;/p&gt;

&lt;p&gt;My core requirement was much narrower:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reactive updates for shared data across screens, with stable identity.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;MobX handled that part extremely well.&lt;br&gt;
Its fine-grained reactivity and simple mental model made shared data easy to keep in sync.&lt;/p&gt;

&lt;p&gt;Everything else came later — not as a grand design,&lt;br&gt;
but as a response to concrete problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  How extra layers appeared
&lt;/h2&gt;

&lt;p&gt;Following fairly standard clean code principles,&lt;br&gt;
additional layers started to emerge naturally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;normalization to avoid duplicated entity instances&lt;/li&gt;
&lt;li&gt;explicit relationships instead of nested DTO trees&lt;/li&gt;
&lt;li&gt;lifecycle boundaries for long-lived data&lt;/li&gt;
&lt;li&gt;async orchestration to avoid race conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this was planned upfront.&lt;br&gt;
These layers appeared because the same problems kept resurfacing.&lt;/p&gt;

&lt;p&gt;Over time, the codebase wasn’t growing mainly in features —&lt;br&gt;
it was growing in &lt;strong&gt;coordination logic&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lifecycle and ownership
&lt;/h2&gt;

&lt;p&gt;One of the hardest problems turned out to be data lifecycle.&lt;/p&gt;

&lt;p&gt;Entities no longer belonged to a single screen.&lt;br&gt;
They lived longer than any individual UI flow.&lt;/p&gt;

&lt;p&gt;Without explicit rules, this led to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;memory growth with no clear eviction strategy&lt;/li&gt;
&lt;li&gt;accidental retention through forgotten references&lt;/li&gt;
&lt;li&gt;uncertainty around who actually “owns” the data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once lifecycle became an explicit concern,&lt;br&gt;
the system became much easier to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  Making concerns explicit
&lt;/h2&gt;

&lt;p&gt;A real shift happened when these concerns became explicit and composable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;garbage collection strategies&lt;/li&gt;
&lt;li&gt;persistence&lt;/li&gt;
&lt;li&gt;async control (cancel, retry, refresh)&lt;/li&gt;
&lt;li&gt;integration boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treating them as pluggable layers clarified responsibilities&lt;br&gt;
and reduced hidden coupling between features.&lt;/p&gt;

&lt;p&gt;At that point, the structure stopped being tied to a single project.&lt;/p&gt;




&lt;h2&gt;
  
  
  A library as a side effect
&lt;/h2&gt;

&lt;p&gt;Extracting this approach into a small library wasn’t the original goal.&lt;/p&gt;

&lt;p&gt;It happened because the same structure kept reappearing across projects,&lt;br&gt;
and formalizing it made experimentation easier.&lt;/p&gt;

&lt;p&gt;It’s still very much an experiment —&lt;br&gt;
an attempt to validate whether a more explicit,&lt;br&gt;
entity-first domain layer makes sense&lt;br&gt;
for large, long-lived React Native applications.&lt;/p&gt;

&lt;p&gt;If you’re curious, I ended up extracting this approach into a small open-source experiment:&lt;br&gt;
&lt;a href="https://github.com/nexigenjs/entity-normalizer" rel="noopener noreferrer"&gt;https://github.com/nexigenjs/entity-normalizer&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Open questions
&lt;/h2&gt;

&lt;p&gt;I don’t think there’s a single correct answer here.&lt;/p&gt;

&lt;p&gt;I’m curious how others approach this today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When does client-side entity normalization start paying off for you?&lt;/li&gt;
&lt;li&gt;Where do you draw the line between server cache and domain entities?&lt;/li&gt;
&lt;li&gt;How do you handle lifecycle and ownership of shared client-side data?&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>react</category>
      <category>reactnative</category>
    </item>
  </channel>
</rss>
