Skip to main content

Query hooks

Query hooks for each collection in your schema are generated by the CLI with specific names. See sections below for a more detailed API.

For example, if you have a collection named todoItems, you will get these query hooks:

  • useTodoItem: Retrieves one document. You pass in an id.
  • useOneTodoItem: pass an index query to filter the list of returned items, and only take the first match.
  • useAllTodoItems: pass an index query to filter the list of returned items.
  • useAllTodoItemsPaginated: pass an index query to filter the list of returned items. Paginates data according to the pageSize parameter. When the next page is fetched, the prior page is discarded. Good for paginated interfaces. Returns a tuple: [page, tools].
  • useAllTodoItemsInfinite: pass an index query to filter the list of returned items. Paginates data according to the pageSize parameter, but appends newly fetched pages to the end of the list instead of replacing the returned set. Good for infinite scrolling lists. Returns a tuple: [itemsSoFar, tools].

Suspense

The hooks use Suspense so that you don't have to write loading state conditional code in your components. All hooks return data directly. If the data is not ready, they suspend.

Wrap your app in a <Suspense> to handle this. You can create multiple layers of Suspense to handle loading more granularly.

Opting out of Suspense

In addition to just not liking Suspense, there are various reasons you may want to opt out for specific queries. Each query hook has an equivalent hook with the word Unsuspended appended to the name. These hooks will return { data, status } instead of the dataset directly. data will be null for single-item queries and [] for list queries, until status is "ready". Possible status values are: "initial", "initializing", "revalidating", and "ready".

NOTE: presently, there are no Unsuspended versions of paginated queries. Let me know if you really need those in a Github issue.

Query hook types in-depth

use___

Pass a document's primary key to load it. If the document does not exist, returns null. Keep in mind that if you know the primary key ahead of time, a document may not be available until it is synced from a peer. You should always handle the null case.

useOne___

Use an index filter to select the first matching document. If no filter is passed, an arbitrary document is returned. Returns null if no matching document exists.

useAll___

Load all documents, or use an index filter to load a subset. Returns all matched documents as an array.

useAll___Paginated

Load a page of documents, with or without an index filter. Returns a tuple. The first tuple value is an array of documents representing the current page. The second value is an object of "tools" for manipulating pagination:

{
status, // the query status (initial, initializing, ready, revalidating)
// this will be "revalidating" while a new page is being fetched
hasNext, // a boolean indicating if a next page is available
hasPrevious, // a boolean indicating if a previous page is available
next, // call this function to advance to the next page (no-op if none exists)
previous, // call this function to go back to the previous page (no-op if none exists)
setPage, // call this with a page index to jump to a page
}

useAll___Infinite

Loads a page of documents, with or without an index filter. When additional pages are loaded, they are appended to the result set, rather than replacing it. Returns a tuple. The first tuple value is an array of documents representing the current page. The second value is an object of "tools" for manipulating pagination:

{
status, // the query status (initial, initializing, ready, revalidating)
// this will be "revalidating" while a new page is being fetched
hasMore, // a boolean indicating if more data is available to load
loadMore, // call this function to load the next page of data
}

Query reactivity

When using hooks to run queries in React, the hook will only re-render your React component when the set of documents returned by the query changes. It will not re-render your component if the contents of those documents change; to monitor document data, you should pass a document to useWatch.

Query keys and identity

TL;DR: When query index filters are dynamic, pass key to the hook to prevent memory and CPU waste or unexpected React suspense triggering.

React bindings for Verdant queries provide a high level of convenience for altering query index filters on the fly, but there are caveats.

Consider the following example:

const [inputValue, setInputValue] = useState('');
const posts = hooks.useAllPosts({
index: {
where: 'titleMatch',
eq: inputValue,
},
});

If inputValue is connected to a user-facing input, as the user types, this value will change rapidly. Because the filter of the query changes, its automatically computed cache key will also change, so the prior query will actually be discarded with each keystroke. This is wasteful!

To prevent this, you should pass a key value to the hook. When a key is specified, the query is retained and its filter is updated, instead of launching a new query. Your key should be unique to the component / usage context of this particular query. For example, "postsFilteredByInput" might be a good key for the example above.

In the future, default key value behavior and/or the optionality of key in these hooks may change. I'm still working on the ergonomics.

Sharing keys to reference the same query in different places

Since queries are cached by key, once you've specified a key value for a query hook somewhere, using the same key elsewhere will immediately load the in-memory cached query. You can exploit this to move queries downward in your React tree without performance loss: rather than have one query at the top of the app for commonly used data and passing that down, you can encapsulate this query in a reusable hook with a hardcoded key and call it freely from any component in your app.

export function useFilteredPosts() {
// this is just for example's sake, suppose we have a search string provided
// by some app context state to use.
const { filterValue } = useFilterContext();
return hooks.useAllPosts({
index: {
where: 'titleMatch',
eq: filterValue,
},
key: 'filteredPosts',
});
}

Don't overdo it -- be careful not to reuse the same key if different subscribers to this query may expect different data, for example if a different filter value is supplied in different parts of the app. Once a key is applied, all usages of query hooks using that key will receive the exact same data.

Query disposal and keep-alive

Utilizing client.queries.keepAlive can help manage query disposal behavior. Normally, once a React component which used a query is unmounted, if no other subscribing components exist, the query will be disposed after 5 seconds. Remounting that component later will require reloading the entire query from disk.

To prevent this disposal, you can use client.queries.keepAlive, passing in the query's key. This is best done with custom keys.

A good place to put keepAlive might be in a top-level route component in a nested route structure. For example, suppose you have a route structure of /posts/:postId. In your /posts page, you query all posts with a filter, but on the /posts/:postId page, the component which performed that query is unmounted to show the individual post. In this case, when users view a specific post for more than 5 seconds, the filtered post query is unloaded. When they navigate back to /posts, the app has to re-fetch the posts list.

You can optimize this by keeping the posts list query in memory with a keep-alive. Find a common ancestor React component which remains mounted on both the /posts and /posts/:postId pages, and include something like this:

const client = hooks.useClient();
useEffect(() => {
client.queries.keepAlive('postsList');
return () => client.queries.dropKeepAlive('postsList');
}, [client]);

This useEffect will mark a keep-alive hold on the query with the key postsList while the component is mounted, then remove the hold after it unmounts. As long as this hook is run in a component which stays mounted on both route paths, users can remain on the post page as long as they want and the post list query will remain fresh.

Keep in mind that you shouldn't just put this at the global app level, unless you're ok with the post list query remaining in-memory indefinitely. Where you place this hook makes all the difference.

You could also place this hook within the post page component! As long as there is less than 5 seconds between the posts list unmounting and the post page mounting, the keep alive should come into effect before the list query is disposed.

Keep-alive outside React hooks

The example above uses React hooks to place and remove keep-alive holds. You can also reference the client directly with a query key to do this. Keep in mind you need a reference to the resolved, initialized Client, not the ClientDescriptor. React normally uses Suspense to avoid thinking about that, but if you need the Client outside React, you'll need something like:

const client = await clientDesc.open();
client.queries.keepAlive('some-key');

This is true of Verdant vanilla JS usage generally, but since you're in the React docs you may not be aware of that.

I'm looking into ways of removing this extra await step since it's very inconvenient.