Skip to Content
LearnCore ConceptsIdentity Mapping

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 behavior

The 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') // Instant

Proxy 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 behavior

With 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 behavior

Best 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 changes

DON’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 instance

Next Steps

Last updated on