Skip to Content
GuidesClient GuidesRelationships

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 configuration

API 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 relationships

Next Steps

Last updated on