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/react
It 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 permissions
Return 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>;