Skip to main content
Version: 6.5

Relational data

Rest Hooks handles one-to-one, many-to-one and many-to-many relationships on entities using Entity.schema

Nesting

Nested members are hoisted during normalization when Entity.schema is defined. They are then rejoined during denormalization

Diagram
 
Fixtures
GET /posts
[{"id":"1","title":"My first post!","author":{"id":"123","name":"Paul"},"comments":[{"id":"249","content":"Nice post!","commenter":{"id":"245","name":"Jane"}},{"id":"250","content":"Thanks!","commenter":{"id":"123","name":"Paul"}}]},{"id":"2","title":"This other post","author":{"id":"123","name":"Paul"},"comments":[{"id":"251","content":"Your other post was nicer","commenter":{"id":"245","name":"Jane"}},{"id":"252","content":"I am a spammer!","commenter":{"id":"246","name":"Spambot5000"}}]}]
api/Post
import { Entity } from '@rest-hooks/rest';
export class User extends Entity {
id = '';
name = '';
pk() {
return this.id;
}
}
export class Comment extends Entity {
id = '';
content = '';
commenter = User.fromJS();
pk() {
return this.id;
}
static schema = {
commenter: User,
};
}
export class Post extends Entity {
id = '';
title = '';
author = User.fromJS();
comments: Comment[] = [];
pk() {
return this.id;
}
static schema = {
author: User,
comments: [Comment],
};
}
export const PostResource = createResource({
path: '/posts/:id',
schema: Post,
});
PostPage
import { PostResource } from './api/Post';
function PostPage() {
const posts = useSuspense(PostResource.getList);
return (
<div>
{posts.map(post => (
<div>
<h4>
{post.title} - <cite>{post.author.name}</cite>
</h4>
<ul>
{post.comments.map(comment => (
<li>
{comment.content}{' '}
<small>
<cite>
{comment.commenter.name}
{comment.commenter === post.author ? ' [OP]' : ''}
</cite>
</small>
</li>
))}
</ul>
</div>
))}
</div>
);
}
render(<PostPage />);
Live Preview
Loading...
Store
    • {} 0 keys
      • {} 0 keys
        • {} 0 keys
          • {} 0 keys
            • {} 0 keys
              • 0

            Reverse lookups

            Even though a response may only nest in one direction, Rest Hooks can handle reverse relationships by overriding Entity.process. Additionally, Entity.merge may need overriding to ensure deep merging of those expected fields.

            This allows you to traverse the relationship after processing only one fetch request, rather than having to fetch each time you want access to a different view.

            Fixtures
            GET /posts
            [{"id":"1","title":"My first post!","author":{"id":"123","name":"Paul"},"comments":[{"id":"249","content":"Nice post!","commenter":{"id":"245","name":"Jane"}},{"id":"250","content":"Thanks!","commenter":{"id":"123","name":"Paul"}}]},{"id":"2","title":"This other post","author":{"id":"123","name":"Paul"},"comments":[{"id":"251","content":"Your other post was nicer","commenter":{"id":"245","name":"Jane"}},{"id":"252","content":"I am a spammer!","commenter":{"id":"246","name":"Spambot5000"}}]}]
            api/Post
            import { Entity } from '@rest-hooks/rest';
            export class User extends Entity {
            id = '';
            name = '';
            posts: Post[] = [];
            comments: Comment[] = [];
            pk() {
            return this.id;
            }
            static merge(existing, incoming) {
            return {
            ...existing,
            ...incoming,
            posts: [...(existing.posts || []), ...(incoming.posts || [])],
            comments: [...(existing.comments || []), ...(incoming.comments || [])],
            };
            }
            static process(value, parent, key) {
            switch (key) {
            case 'author':
            return { ...value, posts: [parent.id] };
            case 'commenter':
            return { ...value, comments: [parent.id] };
            default:
            return { ...value };
            }
            }
            }
            export class Comment extends Entity {
            id = '';
            content = '';
            commenter = User.fromJS();
            post = Post.fromJS();
            pk() {
            return this.id;
            }
            static schema: Record<string, Schema> = {
            commenter: User,
            };
            static process(value, parent, key) {
            return { ...value, post: parent.id };
            }
            }
            export class Post extends Entity {
            id = '';
            title = '';
            author = User.fromJS();
            comments: Comment[] = [];
            pk() {
            return this.id;
            }
            static schema = {
            author: User,
            comments: [Comment],
            };
            }
            // with cirucular dependencies we must set schema after they are all defined
            User.schema = {
            posts: [Post],
            comments: [Comment],
            };
            Comment.schema = {
            ...Comment.schema,
            post: Post,
            };
            export const PostResource = createResource({
            path: '/posts/:id',
            schema: Post,
            dataExpiryLength: Infinity,
            });
            export const UserResource = createResource({
            path: '/users/:id',
            schema: User,
            });
            UserPage
            import { UserResource } from './api/Post';
            export default function UserPage({ setRoute, id }) {
            const user = useSuspense(UserResource.get, { id });
            return (
            <div>
            <h4>
            <a onClick={() => setRoute('page')} style={{ cursor: 'pointer' }}>
            &lt;
            </a>{' '}
            {user.name}
            </h4>
            {user.posts.length ? (
            <>
            <h5>Posts</h5>
            <ul>
            {user.posts.map(post => (
            <li>{post.title}</li>
            ))}
            </ul>
            </>
            ) : null}
            <h5>Comments</h5>
            <ul>
            {user.comments.map(comment => (
            <li>{comment.content}</li>
            ))}
            </ul>
            </div>
            );
            }
            PostPage
            import { PostResource } from './api/Post';
            export default function PostPage({ setRoute }) {
            const posts = useSuspense(PostResource.getList);
            return (
            <div>
            {posts.map(post => (
            <div>
            <h4>
            {post.title} -{' '}
            <cite
            onClick={() => setRoute(`user/${post.author.id}`)}
            style={{ cursor: 'pointer', textDecoration: 'underline' }}
            >
            {post.author.name}
            </cite>
            </h4>
            <ul>
            {post.comments.map(comment => (
            <li>
            {comment.content}{' '}
            <small>
            <cite
            onClick={() => setRoute(`user/${comment.commenter.id}`)}
            style={{ cursor: 'pointer', textDecoration: 'underline' }}
            >
            {comment.commenter.name}
            {comment.commenter === post.author ? ' [OP]' : ''}
            </cite>
            </small>
            </li>
            ))}
            </ul>
            </div>
            ))}
            </div>
            );
            }
            Navigation
            import PostPage from './PostPage';
            import UserPage from './UserPage';
            function Navigation() {
            const [route, setRoute] = React.useState('posts');
            if (route.startsWith('user'))
            return <UserPage setRoute={setRoute} id={route.split('/')[1]} />;
            return <PostPage setRoute={setRoute} />;
            }
            render(<Navigation />);
            Live Preview
            Loading...
            Store
              • {} 0 keys
                • {} 0 keys
                  • {} 0 keys
                    • {} 0 keys
                      • {} 0 keys
                        • 0

                      Circular dependencies

                      Because circular imports and circular class definitions are not allowed, sometimes it will be necessary to define the schema after the Entities definition.

                      api/Post.ts
                      import { Entity } from '@rest-hooks/rest';
                      import { User } from './User';

                      export class Post extends Entity {
                      id = '';
                      title = '';
                      author = User.fromJS();

                      pk() {
                      return this.id;
                      }

                      static schema = {
                      author: User,
                      };
                      }

                      // both User and Post are now defined, so it's okay to refer to both of them
                      User.schema = {
                      // ensure we keep the 'createdAt' member
                      ...User.schema,
                      posts: [Post],
                      };
                      api/User.ts
                      import { Entity } from '@rest-hooks/rest';
                      import type { Post } from './Post';
                      // we can only import the type else we break javascript imports
                      // thus we change the schema of UserResource above

                      export class User extends Entity {
                      id = '';
                      name = '';
                      posts: Post[] = [];
                      createdAt = new Date(0);

                      pk() {
                      return this.id;
                      }

                      static schema: Record<string, Schema | Date> = {
                      createdAt: Date,
                      };
                      }