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.valueisundefined - Background: Kicks off hydration automatically
- Second render:
issue.author.valueis loadedUserobject - 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
.suspenseinstead 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 immediatelyStep 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 storageStep 4: MobX re-renders
// When data arrives:
runInAction(() => {
this.items = loadedIssues // Update observable
})
// MobX detects change → Component re-rendersStep 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 hydrationCollections (Suspense)
const issues = team.issues.suspense // Throws promise if not loaded
// Only renders when data readyReferences (Progressive)
const author = issue.author.value // undefined → kicks hydration
const name = issue.author.value?.name // Handle with optional chainingReferences (Suspense)
const author = issue.author.suspense // Throws promise if not loaded
const name = author.name // Guaranteed to existNested 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).textThe 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
| Approach | API Style | Loading in Components | Complexity |
|---|---|---|---|
| useState + useEffect | Async | Manual everywhere | High |
| React Query | Async | useQuery hooks | Medium |
| Apollo GraphQL | Async | useQuery hooks | Medium |
| Gluonic Progressive | Sync | Inline with optionals | Low |
| Gluonic Suspense | Sync | Suspense boundaries | Low |
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
- Learn about Object Graph - The structure that enables this
- Learn about Lazy Collections - How collections load on-demand
- Learn about Lazy References - How references work
- Learn about React Suspense - Using Suspense boundaries
- See examples - See it in action