When client-side entity normalization actually becomes necessary in large React Native apps
Source: Dev.to
Background
Over the past few years, while working on several React Native projects—different products, different teams—I kept encountering very similar symptoms.
As the apps grew, the same entities began to appear in more and more places:
- feeds
- detail screens
- search results
- notifications
- background updates
Each new feature introduced small, reasonable decisions:
- cache a list response
- refetch on screen focus
- merge partial updates
- add derived selectors
- manually sync data between screens
Individually, these choices made sense. Collectively, they were all trying to solve the same underlying problem. At some point it became clear this wasn’t accidental anymore.
When Normalization Felt Unnecessary
For a long time, entity normalization seemed unnecessary when the data:
- belongs to a single screen
- has a short lifecycle
- isn’t reused elsewhere
In those cases, keeping the data close to the API response works perfectly fine, and normalization would mostly add ceremony without much payoff.
When Normalization Becomes Necessary
The problem started once data stopped being screen‑local. Libraries like Redux Toolkit or React Query are popular for good reasons, but their strength is also their breadth.
My core requirement was much narrower: reactive updates for shared data across screens, with stable identity.
-
MobX handled that part extremely well.
-
The rest of the architecture emerged later, following fairly standard clean‑code principles:
- normalization to avoid duplicated entity instances
- explicit relationships instead of nested DTO trees
- lifecycle boundaries for long‑lived data
- async orchestration to avoid race conditions
None of this was planned upfront. Over time, the codebase grew more in coordination logic than in features.
Data‑Lifecycle Challenges
Entities no longer belonged to a single screen. Without explicit rules, this led to:
- memory growth with no clear eviction strategy
- accidental retention through forgotten references
- uncertainty around who actually “owns” the data
When lifecycle became an explicit concern, a real shift happened as these concerns were made explicit and composable:
- garbage‑collection strategies
- persistence
- async control (cancel, retry, refresh)
- integration boundaries
Treating them as pluggable layers clarified responsibilities.
Extracting the Approach
At that point, the structure stopped being tied to a single project. Extracting this approach into a small library wasn’t the original goal; it happened because the same structure kept reappearing across projects. It’s still very much an experiment.
If you’re curious, I extracted the approach into a small open‑source experiment:
https://github.com/nexigenjs/entity-normalizer
Open Questions
I don’t think there’s a single correct answer here. I’m curious how others approach this today:
- When does client‑side entity normalization start paying off for you?
- Where do you draw the line between server cache and domain entities?
- How do you handle lifecycle and ownership of shared client‑side data?