Relationships
Work with related data using lazy references and collections.
API Reference: @ManyToOne · @OneToMany · LazyReference · LazyCollection
Overview
Gluonic supports standard database relationships:
- Many-to-One - Post → Author (User)
- One-to-Many - User → Posts (Post[])
- One-to-One - User → Profile
- Many-to-Many - Post → Tags (coming soon)
All relationships are lazy-loaded - they load in the background when first accessed.
Many-to-One
A post belongs to one author:
@ClientModel('post')
export class Post extends Model {
@Property()
authorId?: string // Optional FK
@ManyToOne('user', 'authorId', 'posts')
author?: User // Optional relationship (matches FK)
}
// Usage - Progressive pattern
const PostDetail = observer(({ post }) => {
return (
<div>
<h1>{post.title}</h1>
<p>By: {post.author.value?.name ?? 'Loading...'}</p>
</div>
)
})
// Usage - Suspense pattern
<Suspense fallback={<Spinner />}>
<PostDetail post={post} />
</Suspense>
const PostDetail = observer(({ post }) => {
const author = post.author.suspense // Guaranteed loaded
return (
<div>
<h1>{post.title}</h1>
<p>By: {author.name}</p>
</div>
)
})Parameters:
'user'- Related type name (from@ClientModel('user'))'authorId'- Foreign key field on Post'posts'- Inverse collection name on User (optional)
One-to-Many
A user has many posts:
@ClientModel('user')
export class User extends Model {
@OneToMany('post', 'author')
posts: LazyCollection<Post> = new LazyCollection<Post>()
}
// Usage
const UserPosts = observer(({ user }) => {
return (
<div>
<h2>{user.name}'s Posts</h2>
{user.posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
)
})Parameters:
'post'- Related type name (from@ClientModel('post'))'author'- Inverse property name on Post (optional)
Lazy Loading Behavior
Auto-Kick on Access
const post = useModel<Post>('post', '123')
// First access
console.log(post.author.value) // undefined
// Triggers background load automatically
// 100ms later...
console.log(post.author.value) // User { ... } ✓Loading States
const PostAuthor = observer(({ post }) => {
if (post.author.isLoading) {
return <Spinner />
}
if (post.author.error) {
return <Error message={post.author.error.message} />
}
if (!post.author.value) {
return <Text>No author</Text>
}
return <Text>By: {post.author.value.name}</Text>
})Manual Hydration
// Load before rendering
useEffect(() => {
post.author.hydrate()
}, [post])
// Or hydrate entire model
useEffect(() => {
post.hydrate(2) // Depth 2: post.author, post.comments, post.comments[].author
}, [post])Collection Methods
LazyCollection extends Array:
const user = useModel<User>('user', userId)
// Array methods (auto-hydrate)
user.posts.map(p => p.title)
user.posts.filter(p => p.published)
user.posts.find(p => p.id === '123')
user.posts.length
// Check loading state
user.posts.isLoaded
user.posts.isLoading
// Manual hydration
await user.posts.hydrate()Nested Relationships
const PostWithEverything = observer(({ post }) => {
return (
<div>
{/* Post data */}
<h1>{post.title}</h1>
{/* Many-to-one: author */}
<p>By: {post.author.value?.name}</p>
{/* One-to-many: comments */}
{post.comments.map(comment => (
<div key={comment.id}>
<p>{comment.text}</p>
{/* Nested many-to-one: comment author */}
<small>{comment.author.value?.name}</small>
</div>
))}
</div>
)
})Performance
Batch Loading
When accessing collections for multiple models:
const users = useCollectionModels<User>('user')
// Loads posts for all users in single request âś“
users.forEach(user => {
console.log(user.posts.length)
})
// Requires server batch loading configurationAPI Reference: See Server Batch Loading for configuration details
Avoid Deep Hydration
// ❌ Bad - loads entire graph
await post.hydrate(10)
// âś… Good - load only what you need
await post.hydrate(1) // Just direct relationshipsNext Steps
- Offline Handling - Network state
- Batch Loading - Server-side optimization
- Lazy Loading Concept - Deep dive
Last updated on