Skip to Content
LearnCore ConceptsSynchronous API

Synchronous API

Gluonic’s differentiator: Access asynchronously-loaded data through a synchronous API.

The Problem with Typical Sync Engines

Most sync engines require async patterns everywhere:

// Traditional sync engine approach const TeamView = ({ teamId }) => { const [team, setTeam] = useState(null) const [issues, setIssues] = useState([]) const [loading, setLoading] = useState(true) useEffect(() => { Promise.all([ syncEngine.getTeam(teamId), syncEngine.getIssues(teamId) ]).then(([teamData, issuesData]) => { setTeam(teamData) setIssues(issuesData) setLoading(false) }) }, [teamId]) if (loading) return <Spinner /> return ( <div> <h1>{team.name}</h1> {issues.map(issue => <IssueRow key={issue.id} issue={issue} />)} </div> ) }

Problems:

  • Manual loading states everywhere
  • Complex async logic in every component
  • useState/useEffect boilerplate
  • Hard to maintain

The Gluonic Solution

Gluonic provides two ways to work with async data synchronously:

Pattern 1: Progressive Enhancement (Eventual Consistency)

Handle loading states inline - data appears as it loads:

// Data may be undefined initially - handle gracefully const TeamView = observer(({ teamId }) => { const team = useModel<Team>('team', teamId) const issues = useCollectionModels<Issue>('issue', issue => issue.teamId === teamId ) return ( <div> <h1>{team.name}</h1> {issues.map(issue => ( <IssueRow key={issue.id} issue={issue} author={issue.author.value?.name ?? 'Loading...'} // Optional chaining /> ))} </div> ) })

How it works:

  • First render: issue.author.value is undefined
  • Background: Kicks off hydration automatically
  • Second render: issue.author.value is loaded User object
  • Your code: Handles both states with optional chaining

Best for:

  • Lists that can show empty initially
  • Optional data that enhances the UI
  • Progressive loading experiences

Pattern 2: Guaranteed Data (Suspense Boundaries)

Use Suspense to guarantee data is loaded before rendering:

// Fallback handled outside component <Suspense fallback={<Spinner />}> <TeamView team={team} /> </Suspense> // Inside component - data is guaranteed to be there const TeamView = observer(({ team }) => { const issues = team.issues.suspense // Throws promise until loaded return ( <div> <h1>{team.name}</h1> {issues.map(issue => ( <IssueRow key={issue.id} issue={issue} author={issue.author.suspense.name} // No optional chaining needed! /> ))} </div> ) })

How it works:

  • Access .suspense instead of .value
  • Throws promise if data not loaded
  • React Suspense catches it and shows fallback
  • Once loaded, component renders with guaranteed data
  • Your code: Can assume data exists (no optionals)

Best for:

  • Critical data required for rendering
  • Complex components that can’t handle partial states
  • Cleaner component code (no optionals)

How It Works

Gluonic combines several concepts to make this possible:

1. Object Graph

All data lives in a connected graph of model instances in memory (the ObjectPool). You access data by navigating this graph.

2. Lazy Loading

Collections and references that aren’t loaded yet return empty/undefined immediately, then kick off background loading.

3. MobX Reactivity

When data loads, MobX automatically re-renders components that depend on it.

4. Progressive Enhancement

UI shows immediately with available data, fills in as more data loads.

Step-by-Step Flow

const issues = team.issues.map(issue => issue.title)

Step 1: Access returns immediately (synchronous)

// Returns: [] (empty array) // No await needed - this is synchronous!

Step 2: Hydration kicks off (background)

// Behind the scenes: if (!team.issues.hydrated) { void team.issues.hydrate() // Start async load } return this.items // Return empty immediately

Step 3: Data loads

// Model Loader tries: 1. Local storage via Drizzle adapter (~10ms) 2. Network if not found (~100-500ms) 3. Cache result in local storage

Step 4: MobX re-renders

// When data arrives: runInAction(() => { this.items = loadedIssues // Update observable }) // MobX detects change → Component re-renders

Step 5: UI updates

// Component re-renders const issues = team.issues.map(issue => issue.title) // Returns: ['Fix bug', 'Add feature'] // Still synchronous!

All Data Access is Synchronous

Collections (Progressive)

team.issues.map(i => i.title) // [] → kicks hydration team.issues.filter(i => i.done) // [] → kicks hydration team.issues.length // 0 → kicks hydration

Collections (Suspense)

const issues = team.issues.suspense // Throws promise if not loaded // Only renders when data ready

References (Progressive)

const author = issue.author.value // undefined → kicks hydration const name = issue.author.value?.name // Handle with optional chaining

References (Suspense)

const author = issue.author.suspense // Throws promise if not loaded const name = author.name // Guaranteed to exist

Nested Access

// All synchronous! (Progressive pattern) const authorName = issue.author.value?.name const commentCount = issue.comments.length const firstCommentText = issue.comments.at(0)?.text // All synchronous! (Suspense pattern) const authorName = issue.author.suspense.name const firstCommentText = issue.comments.suspense.at(0).text

The Key Insight

“Synchronous” doesn’t mean “data is always there” - it means “no async/await in your component logic”.

Instead of:

// ❌ Async pattern const [data, loading] = useAsyncData() if (loading) return <Spinner />

You get:

// ✅ Pattern 1: Handle inline with optionals const data = useSyncData() return <div>{data?.value || 'Loading...'}</div> // ✅ Pattern 2: Suspense outside, guaranteed inside <Suspense fallback={<Spinner />}> <Component /> {/* data.suspense is guaranteed */} </Suspense>

Both are “synchronous” in that your component code doesn’t use promises or async/await. The loading complexity is handled either inline (pattern 1) or by React Suspense (pattern 2).

Best Practice: Use progressive enhancement (pattern 1) for better UX. Let data appear as it loads rather than blocking with spinners. Use Suspense (pattern 2) when you need guaranteed data for complex rendering logic.

Comparison

ApproachAPI StyleLoading in ComponentsComplexity
useState + useEffectAsyncManual everywhereHigh
React QueryAsyncuseQuery hooksMedium
Apollo GraphQLAsyncuseQuery hooksMedium
Gluonic ProgressiveSyncInline with optionalsLow
Gluonic SuspenseSyncSuspense boundariesLow

Why This Matters

For sync engines: All sync engines handle offline, real-time, and optimistic updates. What makes Gluonic different is how you access that synchronized data.

For developers: Write clean, synchronous component code while Gluonic handles all the async complexity behind the scenes.

For users: Progressive enhancement means better perceived performance - no waiting for spinners.

Next Steps

Last updated on