Skip to Content
ReferenceClient APILazyReference

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:

ParameterTypeDescription
loader() => Promise<T>Function to load the referenced object
getForeignKey() => string | null | undefinedOptional 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 | undefined

Synchronous accessor - returns value if loaded, undefined otherwise.

Behavior:

  • Returns undefined if 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(): T

Suspense-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): boolean

Check equality with another reference or value.

Parameters:

ParameterTypeDescription
otherLazyReference<T> | Reference<T> | T | undefinedReference, 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:

  1. First access → Returns undefined, starts loading
  2. Loading completes → Pool updates
  3. MobX triggers re-render
  4. 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:

  1. FK is mutated (authorId changes)
  2. Next access to author.value checks FK
  3. FK mismatch detected → _reset() called
  4. Cache cleared, new hydration starts
  5. Returns undefined until 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(): void

Internal method to invalidate cache.

Called when:

  • Foreign key changes
  • Manual invalidation needed

Behavior:

  • Clears _value
  • Resets _hydrated to false
  • Clears _promise

See Also

Last updated on