Skip to Content
LearnCore ConceptsReactive Models

Reactive Models

Gluonic uses MobX-powered reactive models to automatically update your UI when data changes. No manual state management required.

The Problem

Traditional React requires manual state updates:

// Traditional React const [post, setPost] = useState(null) useEffect(() => { // Initial load fetchPost('123').then(setPost) }, []) const handleSave = async () => { const updated = await savePost('123', { title: 'New' }) setPost(updated) // Manual update 😔 } // Every component that shows this post needs: // - useState for the data // - useEffect to load it // - Manual updates on changes

Result: Lots of boilerplate, easy to forget updates, bugs.

The Gluonic Solution

Models are automatically reactive - change the data, UI updates automatically:

// Gluonic const PostDetail = observer(({ postId }) => { // Just read the data const post = useModel<Post>('post', postId) if (!post) return <div>Not found</div> // Update it anywhere, anytime const handleSave = () => { store.save('post', postId, { title: 'New' }) // UI updates automatically ✓ } return <h1>{post.title}</h1> // Re-renders automatically when title changes ✓ })

How It Works

1. Observable Data Pool

The pool is a MobX observable:

class ObjectPool { // MobX map - tracks all reads/writes map = observable.map<string, WireRow>({}, { deep: false }) get(t: string, id: string): WireRow | null { return this.map.get(`${t}:${id}`) || null } upsert(row: WireRow): void { this.map.set(`${row.t}:${row.id}`, row) // MobX broadcasts change to observers ✓ } }

2. Proxy-Based Field Access

Models read from pool via JavaScript Proxy:

const post = new Proxy(new Post(), { get(target, prop) { if (prop === 'title') { // Read from pool (MobX tracks this!) const row = store.pool.get('post', '123') return row?.p.title } }, set(target, prop, value) { if (prop === 'title') { // Write through store store.save('post', '123', { title: value }) return true } } }) // Reading post.title → MobX knows "this component depends on post:123.title" // Writing post.title → MobX notifies all dependent components

3. Observer HOC

Components wrapped with observer() auto-update:

import { observer } from '@gluonic/client' const PostDetail = observer(({ postId }) => { const post = useModel<Post>('post', postId) // This read is tracked: return <h1>{post.title}</h1> // MobX knows: "PostDetail depends on post:123.title" }) // Later, when data changes: store.save('post', '123', { title: 'Updated' }) // MobX detects: "post:123.title changed" // MobX triggers: "PostDetail needs to re-render" // React re-renders: PostDetail component // User sees: Updated title ✓

Complete Flow

// 1. Component mounts const PostDetail = observer(({ postId }) => { const post = useModel<Post>('post', postId) // ↓ GraphBridge.getModel() // ↓ Returns Proxy wrapping Post instance // 2. Component reads field return <h1>{post.title}</h1> // ↓ Proxy GET trap // ↓ store.pool.get('post', '123').p.title // ↓ MobX tracks: "PostDetail reads post:123.title" }) // 3. Data changes (anywhere in the app) await store.save('post', '123', { title: 'New Title' }) // ↓ Pool updates // ↓ store.pool.upsert({ t: 'post', id: '123', p: { title: 'New Title' } }) // ↓ MobX detects: "post:123.title changed" // ↓ MobX finds: "PostDetail depends on post:123.title" // ↓ MobX schedules: Re-render PostDetail // 4. Component re-renders // Reads post.title again → gets "New Title" // UI updates automatically ✓

Benefits

No Manual State Management

// ❌ Traditional const [post, setPost] = useState() const [loading, setLoading] = useState(true) useEffect(() => { ... }, []) const handleSave = () => { setPost({ ...post, title: 'New' }) // Manual update } // ✅ Gluonic const post = useModel<Post>('post', postId) const handleSave = () => { store.save('post', postId, { title: 'New' }) // That's it! UI updates automatically ✓ }

Precise Re-Renders

Only components that read changed data re-render:

// Component A reads post.title const A = observer(() => { const post = useModel<Post>('post', '123') return <h1>{post.title}</h1> }) // Component B reads post.content const B = observer(() => { const post = useModel<Post>('post', '123') return <p>{post.content}</p> }) // Update title store.save('post', '123', { title: 'New' }) // Result: // - Component A re-renders ✓ (reads title) // - Component B doesn't re-render ✓ (doesn't read title) // - Efficient! 🚀

Works Across Component Tree

Data changes propagate automatically:

// Deeply nested component const DeepComponent = observer(() => { const post = useModel<Post>('post', '123') return <span>{post.title}</span> }) // Top-level action const Header = () => { const handleEdit = () => { store.save('post', '123', { title: 'New' }) // DeepComponent updates automatically ✓ // No prop drilling needed ✓ } return <button onClick={handleEdit}>Edit</button> }

MobX Basics

Observer HOC

Rule: Any component that reads model data MUST be wrapped with observer():

// ✅ Correct const PostTitle = observer(({ postId }) => { const post = useModel<Post>('post', postId) return <h1>{post.title}</h1> // Will update ✓ }) // ❌ Wrong const PostTitle = ({ postId }) => { const post = useModel<Post>('post', postId) return <h1>{post.title}</h1> // Won't update! 💥 }

Computed Properties

Models can have computed properties:

@ClientModel('post') export class Post extends Model { @Property() content: string = '' // Computed (no decorator needed) get wordCount(): number { return this.content.split(/\s+/).length } get readTime(): string { const minutes = Math.ceil(this.wordCount / 200) return `${minutes} min read` } } // In component const PostMeta = observer(({ post }) => { return <span>{post.readTime}</span> // Auto-updates when content changes ✓ })

Batched Updates

MobX batches multiple changes into single re-render:

// Multiple updates in same tick store.save('post', '123', { title: 'New Title' }) store.save('post', '123', { content: 'New Content' }) store.save('post', '123', { published: true }) // MobX batches these // Component re-renders ONCE (not 3 times) ✓

Best Practices

DO ✅

// Wrap components with observer const Component = observer(() => { ... }) // Read data directly const post = useModel<Post>('post', postId) return <h1>{post.title}</h1> // Use computed properties get fullName() { return `${this.firstName} ${this.lastName}` } // Trust MobX batching store.save('post', id, { title: 'A' }) store.save('post', id, { content: 'B' }) // Batched automatically ✓

DON’T ❌

// Don't forget observer() const Component = () => { ... } // ❌ Won't update! // Don't use useState for model data const [title, setTitle] = useState(post.title) // ❌ Redundant // Don't manually trigger re-renders const [, forceUpdate] = useReducer(x => x + 1, 0) store.save(...) forceUpdate() // ❌ Unnecessary // Don't destructure models const { title, content } = post // ❌ Breaks reactivity! // Use: post.title, post.content

Debugging Reactivity

Check if Component is Observer

// In component console.log('Is observer?', Component.isMobXReactObserver)

Track What’s Being Observed

import { trace } from 'mobx' const Component = observer(() => { trace() // Logs all observed values const post = useModel<Post>('post', postId) return <h1>{post.title}</h1> })

Verify Pool is Observable

// Check pool reactivity console.log(isObservable(store.pool.map)) // Should be true

Performance

Reactive models are extremely efficient:

  • Proxy access: < 0.001ms per field read
  • MobX tracking: < 0.001ms per access
  • Re-render decision: < 0.01ms
  • Identity mapping: O(1) lookup

Benchmark: 1000 models with 100 fields each = 5ms total instantiation time.

Next Steps

Last updated on