Identity Mapping
Identity mapping ensures that the same data ID always returns the same JavaScript object instance. This enables efficient React rendering and predictable behavior.
The Problem
Without identity mapping, you get multiple instances:
// Without identity mapping
const user1 = getUser('user-123')
const user2 = getUser('user-123')
console.log(user1 === user2) // false 😔
// Different instances for same data!
// Problems:
// - React can't use === for memoization
// - More memory usage (duplicates)
// - Inconsistent state (updates to user1 don't affect user2)
// - Confusing behaviorThe Gluonic Solution
Same ID → Same instance (always):
// With Gluonic
const user1 = useModel<User>('user', 'user-123')
const user2 = useModel<User>('user', 'user-123')
console.log(user1 === user2) // true ✓
// Exact same JavaScript object!How It Works
IdentityMap Identity Map
IdentityMap maintains a cache of model instances:
class IdentityMap {
// Cache: "type:id" → Model instance
private cache = new Map<string, Model>()
getModel<T>(type: string, id: string): T {
const key = `${type}:${id}`
// Check cache first
let instance = this.cache.get(key)
if (!instance) {
// Not cached - create new instance
instance = instantiateModel(store, type, id)
// Cache it
this.cache.set(key, instance)
}
// Return cached instance
return instance as T
}
}Instance Lifecycle
// First call - creates instance
const post1 = bridge.getModel<Post>('post', '123')
// - Checks cache → miss
// - Creates new Post instance
// - Wraps in Proxy
// - Stores in cache
// - Returns proxy
// Second call - returns cached
const post2 = bridge.getModel<Post>('post', '123')
// - Checks cache → hit! ✓
// - Returns same instance
// - No creation needed
console.log(post1 === post2) // true ✓Benefits
🎯 Efficient React Rendering
React can use referential equality:
const PostList = observer(() => {
const posts = useCollectionModels<Post>('post')
return (
<FlatList
data={posts}
// React can use === to skip re-rendering unchanged items
keyExtractor={post => post.id}
renderItem={({ item }) => <PostItem post={item} />}
/>
)
})
// When one post changes:
// - Other posts have same instance (===)
// - React skips re-rendering them ✓
// - Only changed post re-renders ✓🧠 Predictable Behavior
Updates in one place affect everywhere:
// Component A
const PostTitle = observer(() => {
const post = useModel<Post>('post', '123')
return <h1>{post.title}</h1>
})
// Component B (different file)
const EditPost = () => {
const post = useModel<Post>('post', '123')
const handleEdit = () => {
store.save('post', '123', { title: 'Updated' })
// Component A updates automatically ✓
// Because it's the SAME instance ✓
}
}💾 Memory Efficiency
No duplicate objects in memory:
// Without identity mapping
useModel('user', '123') // Instance 1
useModel('user', '123') // Instance 2 (duplicate!)
useModel('user', '123') // Instance 3 (duplicate!)
// 3× memory usage 😔
// With identity mapping
useModel('user', '123') // Instance 1
useModel('user', '123') // Same instance 1 ✓
useModel('user', '123') // Same instance 1 ✓
// 1× memory usage 🎉🔄 Automatic Synchronization
All references stay in sync:
const postInList = posts.find(p => p.id === '123')
const postInDetail = useModel<Post>('post', '123')
// Same instance!
console.log(postInList === postInDetail) // true ✓
// Update one
store.save('post', '123', { title: 'New' })
// Both update automatically ✓
console.log(postInList.title) // 'New'
console.log(postInDetail.title) // 'New'Cache Invalidation
When data changes, cache is intelligently invalidated:
// Update post
store.save('post', '123', { title: 'Updated' })
// IdentityMap invalidates cache entry
bridge.invalidate('post', '123')
// Next getModel() creates fresh instance
const post = bridge.getModel<Post>('post', '123')
// New instance with updated data ✓Note: Cache invalidation is automatic - you don’t need to manage it.
Garbage Collection
Unused instances are automatically cleaned up:
// Instance created
const post = useModel<Post>('post', '123')
// Cached in IdentityMap
// Component unmounts
// No more references to post instance
// JavaScript GC runs
// Instance is collected (cache uses WeakMap-like behavior)
// Memory freed ✓Implementation Details
Cache Key Format
// Cache key: "type:id"
cache.set('post:123', postInstance)
cache.set('user:abc', userInstance)
cache.set('comment:xyz', commentInstance)
// Lookups are O(1)
const post = cache.get('post:123') // InstantProxy Wrapping
Every instance is wrapped in a Proxy:
const instance = new Post() // Create model
const proxy = new Proxy(instance, {
get(target, prop) {
// Intercept field access
// Read from pool for reactivity
},
set(target, prop, value) {
// Intercept field writes
// Save through store
}
})
// Cache the proxy (not the instance)
cache.set('post:123', proxy)Result: All access goes through proxy → MobX tracking works → UI updates automatically.
Comparison
Without Identity Mapping
class Store {
getModel(type, id) {
const data = this.pool.get(type, id)
return new Model(data) // New instance every time!
}
}
// Problems:
const a = store.getModel('post', '123')
const b = store.getModel('post', '123')
a !== b // Different instances 😔
// React memoization breaks
// More memory usage
// Confusing behaviorWith Identity Mapping (Gluonic)
class IdentityMap {
cache = new Map()
getModel(type, id) {
const key = `${type}:${id}`
if (!this.cache.has(key)) {
this.cache.set(key, createInstance(type, id))
}
return this.cache.get(key) // Same instance ✓
}
}
// Benefits:
const a = bridge.getModel('post', '123')
const b = bridge.getModel('post', '123')
a === b // Same instance ✓
// React memoization works
// Less memory
// Predictable behaviorBest Practices
DO ✅
// Compare by reference
if (post1 === post2) {
// Same post ✓
}
// Pass instances directly
<PostDetail post={post} />
// No need to pass ID and re-fetch
// Trust instance equality
const memoized = useMemo(() => {
return expensiveOperation(post)
}, [post]) // Will only recompute if instance changesDON’T ❌
// Don't compare by ID only
if (post1.id === post2.id) {
// This works, but === is better
}
// Don't create instances manually
const post = new Post() // ❌
post.id = '123'
// Won't be in identity map!
// Use useModel instead
const post = useModel<Post>('post', '123') // ✓
// Don't cache instances yourself
const [cachedPost, setCachedPost] = useState()
// IdentityMap already caches ✓React Integration
Memoization
const PostDetail = observer(({ postId }) => {
const post = useModel<Post>('post', postId)
// Memoize expensive computation
const analysis = useMemo(() => {
return analyzePost(post)
}, [post]) // Only recomputes if instance changes
// Instance only changes when:
// - Cache invalidated
// - Data updated
return <div>{analysis.summary}</div>
})React.memo
// Child component can use React.memo
const PostItem = React.memo(
observer(({ post }: { post: Post }) => {
return <div>{post.title}</div>
})
)
// Parent re-renders
const PostList = observer(() => {
const posts = useCollectionModels<Post>('post')
return (
<div>
{posts.map(post => (
// If post instance unchanged, PostItem skips re-render ✓
<PostItem key={post.id} post={post} />
))}
</div>
)
})Advanced: Cache Strategies
Eager Loading
// Pre-load instances you'll need
await bridge.ensureLoaded('post', '123')
await bridge.ensureLoaded('post', '456')
// Later access is instant (cached)
const post = bridge.getModel('post', '123') // From cache ✓Manual Invalidation
// Force refresh
bridge.invalidate('post', '123')
// Next access creates fresh instance
const post = bridge.getModel('post', '123') // New instanceNext Steps
- Reactive Models - How instances become reactive
- Lazy Loading - On-demand relationship loading
- IdentityMap API - Complete API reference
Last updated on