LazyReference
Lazy-loaded reference to a single related model object. Auto-fetches when accessed.
Import
import { LazyReference } from '@gluonic/client'Class Signature
class LazyReference<T> {
// Constructor
constructor(loader: () => Promise<T>, getForeignKey?: () => string | null | undefined)
// Properties
get value(): T | undefined
get suspense(): T
// Methods
hydrate(): Promise<T>
equals(other: LazyReference<T> | Reference<T> | T | undefined): boolean
// Internal (for FK invalidation)
_reset(): void
}Created by decorators: LazyReference instances are created automatically by relationship decorators (@ManyToOne, @OneToOne). You typically don’t construct them manually.
Constructor
constructor(
loader: () => Promise<T>,
getForeignKey?: () => string | null | undefined
)Create a lazy reference with loader function.
Parameters:
| Parameter | Type | Description |
|---|---|---|
loader | () => Promise<T> | Function to load the referenced object |
getForeignKey | () => string | null | undefined | Optional FK accessor for invalidation |
Example:
// Created by decorator (automatic)
@ManyToOne('user', 'authorId', 'issues')
author?: User // Runtime: LazyReference<User>
// Manual creation (rare)
const authorRef = new LazyReference<User>(
async () => bridge.getModel('user', authorId),
() => this.authorId // FK for invalidation
)Properties
value
get value(): T | undefinedSynchronous accessor - returns value if loaded, undefined otherwise.
Behavior:
- Returns
undefinedif not loaded - Auto-kicks hydration in background on first access
- Returns loaded value after hydration completes
- Auto-invalidates when foreign key changes
- Checks FK on every access (reactive to FK mutations)
Example:
const IssueCard = observer(({ issue }) => {
const author = issue.author.value // User | undefined
return (
<div>
<h2>{issue.title}</h2>
<p>By: {author?.name ?? 'Loading...'}</p>
</div>
)
})FK Invalidation:
// Initially
issue.authorId = 'user-1'
console.log(issue.author.value?.name) // 'Alice'
// Change FK
issue.authorId = 'user-2'
// Next access auto-invalidates and reloads
console.log(issue.author.value) // undefined (loading...)
// After load: 'Bob'suspense
get suspense(): TSuspense-compatible accessor - throws promise if not loaded.
Behavior:
- Returns value if loaded (guaranteed non-undefined)
- Throws promise if not loaded (for React Suspense)
- Auto-invalidates when foreign key changes
- Throws new promise when invalidated
Example:
<Suspense fallback={<Spinner />}>
<IssueDetail issue={issue} />
</Suspense>
const IssueDetail = observer(({ issue }) => {
const author = issue.author.suspense // User (guaranteed loaded)
return (
<div>
<h2>{issue.title}</h2>
<p>By: {author.name}</p> {/* No optional chaining needed */}
</div>
)
})Methods
hydrate()
hydrate(): Promise<T>Explicitly start hydration and return promise.
Returns: Promise<T> - Resolves to loaded value
Behavior:
- Returns immediately if already hydrated
- Starts loading if not hydrated
- Checks FK invalidation before loading
- Returns existing promise if already loading
Example:
// Explicit pre-loading
const author = await issue.author.hydrate()
console.log(author.name) // Guaranteed loaded
// Pre-load on hover
const handleMouseEnter = () => {
void issue.author.hydrate() // Fire and forget
}
// Wait for loading
const [loading, setLoading] = useState(true)
useEffect(() => {
issue.author.hydrate().then(() => setLoading(false))
}, [issue])equals()
equals(other: LazyReference<T> | Reference<T> | T | undefined): booleanCheck equality with another reference or value.
Parameters:
| Parameter | Type | Description |
|---|---|---|
other | LazyReference<T> | Reference<T> | T | undefined | Reference, value, or undefined |
Returns: boolean - true if values are identical (===)
Comparison: Uses object identity, not deep equality
Example:
const issue1Author = issue1.author
const issue2Author = issue2.author
// Compare references
if (issue1Author.equals(issue2Author)) {
console.log('Same author')
}
// Compare with value
if (issue1Author.equals(currentUser)) {
console.log('Assigned to current user')
}Auto-Kick Loading
LazyReference uses auto-kick pattern (like Linear):
const IssueCard = observer(({ issue }) => {
// First render: value is undefined, hydration starts
const author = issue.author.value // undefined
// Component re-renders when hydration completes
// Second render: value is loaded
const author = issue.author.value // User { name: 'Alice' }
return <div>{author?.name ?? 'Loading...'}</div>
})Flow:
- First access → Returns
undefined, starts loading - Loading completes → Pool updates
- MobX triggers re-render
- Second access → Returns loaded value
FK Change Invalidation
LazyReference automatically detects FK changes and invalidates cache:
const IssueEditor = observer(({ issue }) => {
const store = useStore()
// Initially: authorId = 'user-1', author.value = Alice
const handleChangeAuthor = async (newAuthorId: string) => {
await store.save('issue', issue.id, { authorId: newAuthorId })
// author.value auto-invalidates on next access
}
return (
<div>
<p>Current: {issue.author.value?.name ?? 'Loading...'}</p>
<button onClick={() => handleChangeAuthor('user-2')}>
Change to Bob
</button>
</div>
)
})Invalidation Logic:
- FK is mutated (
authorIdchanges) - Next access to
author.valuechecks FK - FK mismatch detected →
_reset()called - Cache cleared, new hydration starts
- Returns
undefineduntil new value loads
Smart Invalidation: Only invalidates when FK actually changes, not on unrelated mutations.
Suspense Integration
LazyReference integrates seamlessly with React Suspense:
const App = () => (
<Suspense fallback={<div>Loading...</div>}>
<IssueDetail issueId="abc-123" />
</Suspense>
)
const IssueDetail = observer(({ issueId }) => {
const issue = useModel<Issue>('issue', issueId)!
// Suspense getter - throws promise if not loaded
const author = issue.author.suspense
const assignee = issue.assignee.suspense
// Both guaranteed loaded here
return (
<div>
<h1>{issue.title}</h1>
<p>Author: {author.name}</p>
<p>Assignee: {assignee.name}</p>
</div>
)
})Common Patterns
Optional Relationship
const IssueCard = observer(({ issue }) => {
const author = issue.author.value
return (
<div>
<h2>{issue.title}</h2>
{author ? (
<UserBadge user={author} />
) : (
<span>No author</span>
)}
</div>
)
})Required Relationship
const IssueCard = observer(({ issue }) => {
const author = issue.author.value
if (!author) return <Spinner /> // Loading
return (
<div>
<h2>{issue.title}</h2>
<UserBadge user={author} />
</div>
)
})Pre-loading
const IssueCard = observer(({ issue }) => {
// Pre-load on mount
useEffect(() => {
void issue.author.hydrate()
}, [issue])
return <div>{issue.author.value?.name ?? 'Loading...'}</div>
})Internal Implementation
Advanced: These details are for understanding, not typical usage.
Internal Properties
_value: T | undefined // Cached value
_hydrated: boolean // Hydration complete flag
_promise?: Promise<T> // In-flight promise_reset()
_reset(): voidInternal method to invalidate cache.
Called when:
- Foreign key changes
- Manual invalidation needed
Behavior:
- Clears
_value - Resets
_hydratedtofalse - Clears
_promise
See Also
- Reference - Eager reference class
- LazyCollection - Lazy collection class
- Decorators - @ManyToOne, @OneToOne decorators
- Lazy References Concept - Conceptual overview
- Relationships Guide - Complete guide