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 thepageSize
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 thepageSize
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.