Over the past few years, while working on several React Native projects,
I kept running into the same type of issues.
Different products, different teams — but very similar symptoms.
At first, none of this felt architectural.
Problems were usually solved locally and pragmatically.
Over time, though, a pattern started to repeat itself.
Repeated solutions, repeated problems
As 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 —
often in slightly different ways and scattered across the codebase.
At some point, it was clear this wasn’t accidental anymore.
It was structural.
When normalization doesn’t matter
For a long time, entity normalization felt unnecessary.
If data:
- belongs to a single screen
- has a short lifecycle
- isn’t reused elsewhere
then keeping it close to the API response works perfectly fine.
In those cases, normalization would mostly add ceremony without much payoff.
The problem started once data stopped being screen-local.
Why popular tools didn’t fully fit this problem
Libraries like Redux Toolkit or React Query are popular for good reasons.
They solve a wide range of problems and do it very well.
That said, their strength is also their breadth.
They answer many questions at once:
server caching, invalidation, background refetching,
pagination, optimistic updates, and more.
My core requirement was much narrower:
Reactive updates for shared data across screens, with stable identity.
MobX handled that part extremely well.
Its fine-grained reactivity and simple mental model made shared data easy to keep in sync.
Everything else came later — not as a grand design,
but as a response to concrete problems.
How extra layers appeared
Following fairly standard clean code principles,
additional layers started to emerge naturally:
- 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.
These layers appeared because the same problems kept resurfacing.
Over time, the codebase wasn’t growing mainly in features —
it was growing in coordination logic.
Lifecycle and ownership
One of the hardest problems turned out to be data lifecycle.
Entities no longer belonged to a single screen.
They lived longer than any individual UI flow.
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
Once lifecycle became an explicit concern,
the system became much easier to reason about.
Making concerns explicit
A real shift happened when these concerns became explicit and composable:
- garbage collection strategies
- persistence
- async control (cancel, retry, refresh)
- integration boundaries
Treating them as pluggable layers clarified responsibilities
and reduced hidden coupling between features.
At that point, the structure stopped being tied to a single project.
A library as a side effect
Extracting this approach into a small library wasn’t the original goal.
It happened because the same structure kept reappearing across projects,
and formalizing it made experimentation easier.
It’s still very much an experiment —
an attempt to validate whether a more explicit,
entity-first domain layer makes sense
for large, long-lived React Native applications.
If you’re curious, I ended up extracting this 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?
Top comments (0)