React
Typesaurus provides first-class support for React (as well as Preact) through @typesaurus/react package.
To start using it, install the package:
npm install --save @typesaurus/reactIt provides a single hook useRead that allows you to fetch once or subscribe to a document or a collection updates:
import { useRead } from "@typesaurus/react";import { schema } from "typesaurus";
const db = schema(($) => ({ posts: collection<Post>(),}));
function Posts() { const [posts] = useRead(db.posts.query(($) => $.field("published").eq(true)));
if (!posts) return <div>Loading...</div>;
return ( <ul> {posts.map((post) => ( <li key={post.ref.id}> <a href={`/posts/${post.ref.slug}`}> <h2>{post.data.title}</h2> <p>{post.data.summary}</p> </a> </li> ))} </ul> );}The useRead hook accepts any reading method, both in promise and subscription mode. It returns a tuple with the result and the status object:
// Subscribe to user updatesconst [user] = useRead(db.users.get(userId).on);
// Get user onceconst [user] = useRead(db.users.get(userId));When running in subscription mode, the hook returns the latest data state and updates it when the document changes.
When the component is unmounted, the subscription is automatically unsubscribed.
Postponing requests
If you need to wait for something, i.e., for userId to resolve, you can pass a falsy value to the useRead:
interface Props { userId: string | null;}
function User({ userId }: Props) { // Wait for userId to resolve, then fetch the user: const [user] = useRead(userId && db.users.get(userId));
if (!user) return <div>Loading...</div>;
return <div>{user.data.name}</div>;}It also works within queries as well:
interface Props { userId: string | null;}
function Posts({ userId }: Props) { // Wait for userId to resolve, then fetch the user's posts: const [posts] = useRead( db.posts.query(($) => userId && $.field("author").eq(userId)), );
if (!posts) return <div>Loading...</div>;
return ( <ul> {posts.map((post) => ( <li key={post.ref.id}> <a href={`/posts/${post.ref.slug}`}> <h2>{post.data.title}</h2> <p>{post.data.summary}</p> </a> </li> ))} </ul> );}Status
Along with the data, useRead returns a status object as the second element of the tuple.
The status object tells if the data is loading or if there was an error:
const [user, { error, loading }] = useRead(db.users.get(userId));
loading;//= false
error;//=> PERMISSION_DENIED: Missing or insufficient permissionsReturn type
The all, query, and many methods return an array of documents or undefined if the query is not ready yet:
const [posts, { error, loading }] = useRead( db.posts.query(($) => $.field("published").eq(true)),);
posts;//=> undefined | Doc<Post>[]The get method returns a single document, undefined if the query is not ready yet, or null if the document is not found:
const [user, { error, loading }] = useRead(db.users.get(userId));
user;//=> undefined | null | Doc<User>useLazyRead
The useLazyRead hook is similar to useRead, but it doesn’t perform the request or subscribe to the updates automatically. Instead, it returns a function that you can call to trigger the request:
import { useLazyRead } from "@typesaurus/react";
interface UIProps { userId: string | null;}
function UI({ userId }: UIProps) { // Create user hook, but don't perform the request yet const useUser = useLazyRead(userId && db.users.get(userId));
if (!user) return <div>Loading...</div>;
return ( <UIContext.Provider value={{ useUser }}> <Content /> </UIContext.Provider> );}
function Content() { // Get the user hook from the context const { useUser } = useContext(UIContext);
// Trigger the request const [user] = useUser(); if (!user) return <div>Loading...</div>; return <div>{user.data.name}</div>;}Here’s an example of how to create the context:
import { dummyLazyReadHook, TypesaurusReact } from "@typesaurus/react";
interface UIContextValue { // Use TypesaurusReact.HookLazyUse useUser: TypesaurusReact.HookLazyUse<Schema["users"]["Result"]>;}
const UIContext = createContext<UIContextValue>({ // Use dummy hook that always returns loading state useUser: dummyLazyReadHook,});You can also delay the request by passing false to the useLazyRead:
function Content({ error }: Props) { const { useUser } = useContext(UIContext); // Skip request to user while there's an error const [user] = useUser(!error);
if (!user) return <div>Loading...</div>; return <div>{user.data.name}</div>;}resolved helper
To know if the document is resolved, you can use the resolved helper:
import { resolved } from "typesaurus";
// Later in the component:
const [user] = useRead(db.users.get(userId));
// resolved(user) will return true if the document finished loadingif (resolved(user) && !user) // So we can show "User not found" message return <div>User not found</div>;