Items
Hooks for loading one item, listing a model's items, and creating and deleting items from React components.
These hooks read and write items in the app's local SQLite database. useItem and useItems keep components in step with the database, and useCreateItem and useDeleteItem wrap Item.create() and item.destroy() with loading and error state. To edit an item's fields, assign to them on the item; see Reading and writing data.
import { useItem, useItems, useCreateItem, useDeleteItem, getItemQueryKey } from '@seedprotocol/react'useItem and useItems wait for the client to be ready before they query. useItems needs a SeedProvider above it and throws without one. useItem works without one on a shared fallback query client, logs a warning once in development builds, and doesn't refresh when the user's addresses change, so keep it inside SeedProvider too.
The examples use the Post type and the asBoolean helper from Reading and writing data.
useItem
Loads one item by model name and ID, and re-renders when any of its property values change, whichever component made the change.
function useItem<T>(props: {
modelName: string
seedLocalId?: string
seedUid?: string
}): {
item: IItem<T> | undefined
isLoading: boolean
error: Error | null
}Parameters
| Name | Type | Default | Description |
|---|---|---|---|
modelName | string | required | The item's model, for example 'Post'. |
seedLocalId | string | The local ID. Takes precedence when both IDs are set. | |
seedUid | string | The EAS attestation UID (0x…), for items that came from the chain. |
While neither ID is set, the hook doesn't query and returns item: undefined with isLoading: false. That makes it safe to call with an empty relation.
Returns
| Field | Type | Description |
|---|---|---|
item | IItem<T> | undefined | The item, or undefined while loading or when nothing matches. |
isLoading | boolean | true until the first lookup finishes, and again while the item reloads. |
error | Error | null | The lookup's error, or Error('Item service error') when the item's state machine fails. |
Behavior
- Cached items come back at once. If the item is already loaded in memory, the first render returns it with
isLoading: false, and the hook refreshes it in the background. - Changing the ID never shows the old item. When
seedLocalIdorseedUidchanges,itemisundefineduntil the new one loads. - Re-renders on value changes. The hook subscribes to each of the item's properties, so an assignment such as
post.title = '…'anywhere in the app re-renders every component that usesuseItemfor that item. - Re-renders after EAS sync. When a sync writes new data for synced items, the hook refetches.
- Re-renders on identity and publish-state changes. While the item is idle, a change to its
seedUid,latestVersionUid,latestVersionLocalId,lastVersionPublishedAt,attestationCreatedAt,revokedAtorpublisherre-renders the hook, whether it comes from sync, another tab, a publish recorded after it finished, or an unpublish. - Holds the item while mounted. Each mounted
useItemholds the item it shows and releases the hold a tick after it unmounts or shows another item, so a StrictMode remount keeps the same instance. An item that no hook or caller holds unloads after the grace period (entityCache.unloadGraceMsinSeedConfig, 60 seconds by default). Writes to an unloaded instance throwEntityUnloadedError; reads return its last values. To keep using the item after the component unmounts, hold it yourself withItem.find({ hold: true })orItem.retain(item), and callunload()when you're done. - Looks the item up again when the instance stops. When the instance it shows stops, because another tab or a local purge removed it or its last hold was released elsewhere, the hook fetches the item again and shows the new instance.
The returned IItem type doesn't list your model's fields. Cast it to your own type, as below.
Example
import { useItem } from '@seedprotocol/react'
import type { Post } from './types'
function PostHeading({ seedLocalId }: { seedLocalId: string }) {
const { item, isLoading, error } = useItem({ modelName: 'Post', seedLocalId })
const post = item as Post | undefined
if (isLoading) return <p>Loading…</p>
if (error) return <p>{error.message}</p>
if (!post) return <p>Post not found</p>
return <h1>{post.title || 'Untitled'}</h1>
}useItems
Lists the items of a model, newest first. The list refreshes when items are created, deleted or synced.
function useItems(props: {
modelName?: string
schemaName?: string
modelFileId?: string
deleted?: boolean
includeEas?: boolean
addressFilter?: 'owned' | 'watched' | 'all'
}): {
items: IItem<any>[]
isLoading: boolean
error: Error | null
}Parameters
| Name | Type | Default | Description |
|---|---|---|---|
modelName | string | The model to list. Leave it out to list items of every model. | |
schemaName | string | With modelName, only that schema's model, for when several schemas define a model with the same name. | |
modelFileId | string | The model's ID (Model.id). An alternative to schemaName. | |
deleted | boolean | false | true lists deleted items instead (for a trash view). |
includeEas | boolean | false | true also lists items that have an EAS attestation UID: published items and items synced from EAS. |
addressFilter | 'owned' | 'watched' | 'all' | 'owned' keeps the items the user owns, by the SDK's ownership rule: drafts that have never had a publisher, and items whose publisher (or, when that's blank, attester) is an owned address. 'watched' keeps items whose publisher is one of the watched addresses. 'all' or leaving it out applies no filter. |
Returns
| Field | Type | Description |
|---|---|---|
items | IItem<any>[] | The items, newest first. [] until the first fetch finishes. |
isLoading | boolean | true until the client is ready and the first fetch has finished. |
error | Error | null | The fetch error, if any. |
Behavior
- Local drafts only by default. Without
includeEas: true, items that have aseedUidare left out, so an item drops out of the list once it's published. PassincludeEas: trueto list drafts and on-chain items together. - Sorting. Items are sorted by the latest published version's time, falling back to the attestation time and then
createdAt, newest first. The times come from the live query, so when a listed item is published or republished the list re-sorts anditemsis a new array. - What triggers a refresh. The hook runs a live query on the
seedstable and refetches only when the set of item IDs changes: an item is created or deleted, or an EAS sync adds items. An item created while the first fetch is still running is listed once that fetch finishes. Editing a field of an existing item doesn't re-render the list. Render each row withuseItemoruseItemPropertywhen the row shows fields that can change. - Revoked items are left out unless
deletedistrue. - Address changes. Inside
SeedProvider, the list refetches when the user's owned or watched addresses change (client.setAddresses(), orclient.init()withaddresses) or local copies are removed. Saving the same addresses again, in another order or letter case, doesn't refetch. - Holds each listed item while mounted. Each mounted
useItemsholds every item in its list and releases an item's hold when the item leaves the list or the hook unmounts. The list's query is dropped as soon as no mounted hook uses it, so a remount fetches the list again, with new instances for items that unloaded in between. When a listed instance stops, the hook fetches the list again. - Unloaded fields. Fields on list items are read from each item's properties and can be
undefineduntil those load. NormaliseListfields before use, for exampleArray.isArray(x) ? x : [].
Before the client is ready, the query doesn't run and the hook returns items: [] with isLoading: true, so check isLoading before showing an empty state.
items keeps its identity while the listed items and their publish times are unchanged, so it can be an effect or useMemo dependency.
Deleting the last item in a list leaves it on screen. The hook ignores a live-query result with no rows while it still holds items, so the list doesn't refetch. Remounting the component, or invalidating ['seed', 'items'] after the delete (see getItemQueryKey), clears it.
Example
import { useItem, useItems } from '@seedprotocol/react'
import type { Post } from './types'
function PostList() {
const { items, isLoading, error } = useItems({ modelName: 'Post', includeEas: true })
if (isLoading) return <p>Loading…</p>
if (error) return <p>Could not load posts: {error.message}</p>
if (items.length === 0) return <p>No posts yet</p>
return (
<ul>
{items.map((item) => (
<PostRow key={item.seedLocalId} seedLocalId={item.seedLocalId} />
))}
</ul>
)
}
function PostRow({ seedLocalId }: { seedLocalId: string }) {
const { item } = useItem({ modelName: 'Post', seedLocalId })
const post = item as Post | undefined
return <li>{post?.title || 'Untitled'}</li>
}A trash view lists deleted items:
import { useItems } from '@seedprotocol/react'
function Trash() {
const { items: trashed } = useItems({ modelName: 'Post', deleted: true })
return <p>{trashed.length} deleted post(s)</p>
}useCreateItem
Returns a createItem function that creates an item with Item.create(), and tracks its loading and error state.
function useCreateItem(): {
createItem: (
modelName: string,
itemData?: Record<string, any>,
options?: ItemCreateOptions,
) => Promise<Item<any> | undefined>
isLoading: boolean
error: Error | null
resetError: () => void
}The return type is exported as UseCreateItemReturn. ItemCreateOptions is exported from @seedprotocol/sdk.
createItem parameters
| Name | Type | Default | Description |
|---|---|---|---|
modelName | string | required | The model to create an item of. |
itemData | Record<string, any> | {} | Initial property values, for example { title: 'Hello' }. Image, File and Html values are saved to files before createItem resolves, as in Item.create(). |
options | ItemCreateOptions | Passed to Item.create(): validate, waitForReady, readyTimeout and hold (Item.create parameters). validate: false writes the values unchecked. hold defaults to false here, where Item.create() defaults to true; hold: true keeps the item loaded until you call item.unload(). |
Returns
| Field | Type | Description |
|---|---|---|
createItem | function | Resolves to the new Item, or undefined on failure. |
isLoading | boolean | true while any create is running, until the last one finishes. |
error | Error | null | The last failure. Cleared when the next create starts. |
resetError | () => void | Sets error back to null. |
createItem never rejects. On failure it resolves to undefined, sets error, and leaves nothing written: Item.create removes the item and any files it saved before it rejects. While a create is still running, a call with the same modelName, the same itemData and the same options returns the first call's promise and creates nothing, even when both calls happen before a re-render (a double click, or two calls in one handler). Any other call creates its own item. Values and options are compared key by key: arrays and plain objects by their contents, so a List value built in the handler still matches, and anything else with Object.is, so a File, Blob or class instance matches only if it's the same object. Both calls must have the same keys; a key set to undefined in one call and absent from the other counts as a different call. Disable the button while isLoading is true, as the example below does, so values that don't match, such as a File the handler builds on each click, can't create a second item. When a create succeeds, useItems lists the new item on its own.
Unless you pass hold: true, nothing holds the new item. Keep its seedLocalId, as the example below does, and render the item with useItem, which holds it while mounted. An instance kept in state or a ref with no hook showing it unloads after the grace period, and the next assignment to it throws EntityUnloadedError. With hold: true it stays loaded until you call item.unload().
error is one of these (Item.create validation has the details; the three error classes are exported from @seedprotocol/sdk):
ItemCreateValidationErrorfor keys the model doesn't have and values that break their property's type or validation rules,Image,FileandHtmlvalues included, unlessoptions.validateisfalse. Itsproblemsarray lists each one as{ property, code, message }.UnknownModelErrorwhen there's no model calledmodelName.ItemCreateErrorwhen saving anImage,FileorHtmlvalue fails for another reason. Itscodeis'save_failed', with the save's own error incause(a URL the browser can't fetch gives aTypeError: Failed to fetch), or'property_not_loaded'when the property didn't load in time.propertynames the value andseedLocalIdis the removed item's.
A URL that answers with an error status, such as a 404, doesn't fail: the response body is saved as the file.
import { ItemCreateError, ItemCreateValidationError } from '@seedprotocol/sdk'
function CreateError({ error }: { error: Error | null }) {
if (error instanceof ItemCreateValidationError) {
return (
<ul role="alert">
{error.problems.map((p, i) => (
<li key={i}>
{p.property}: {p.message}
</li>
))}
</ul>
)
}
if (error instanceof ItemCreateError) {
return <p role="alert">Couldn't save {error.property}. Check the file and try again.</p>
}
return error ? <p role="alert">{error.message}</p> : null
}required isn't checked. A Date property value should be an ISO 8601 string. See What you get back for how each type reads back.
Example
import { useCreateItem } from '@seedprotocol/react'
function NewPostButton({ onCreated }: { onCreated: (id: string) => void }) {
const { createItem, isLoading, error, resetError } = useCreateItem()
const handleClick = async () => {
const post = await createItem('Post', {
title: 'Untitled',
publishedAt: new Date().toISOString(),
})
if (post) onCreated(post.seedLocalId)
}
return (
<>
<button disabled={isLoading} onClick={handleClick}>
{isLoading ? 'Creating…' : 'New post'}
</button>
{error && (
<p role="alert">
{error.message} <button onClick={resetError}>Dismiss</button>
</p>
)}
</>
)
}useDeleteItem
Returns a deleteItem function that deletes an item with item.destroy(), and tracks its loading and error state.
function useDeleteItem(): {
deleteItem: (item: Item<any>) => Promise<void>
isLoading: boolean
error: Error | null
resetError: () => void
}The return type is exported as UseDeleteItemReturn.
Returns
| Field | Type | Description |
|---|---|---|
deleteItem | (item: Item<any>) => Promise<void> | Deletes the item. Does nothing if item is falsy. |
isLoading | boolean | true while the delete runs. |
error | Error | null | The last failure. |
resetError | () => void | Sets error back to null. |
Deleting is a soft delete: the item is marked as deleted in SQLite, drops out of useItems, and shows up in useItems({ deleted: true }). It doesn't affect anything already attested on-chain.
When destroy() fails, for example because the item isn't the user's or the database write fails, deleteItem sets error and rejects, so catch the promise.
deleteItem is typed to take the Item class, while useItem and useItems return the IItem interface. Cast with item as Item<any> (import the type from @seedprotocol/sdk).
Example
import type { Item } from '@seedprotocol/sdk'
import { useDeleteItem, useItem } from '@seedprotocol/react'
function DeletePostButton({ seedLocalId }: { seedLocalId: string }) {
const { item } = useItem({ modelName: 'Post', seedLocalId })
const { deleteItem, isLoading, error } = useDeleteItem()
if (!item) return null
return (
<>
<button
disabled={isLoading}
onClick={() => deleteItem(item as Item<any>).catch(() => {})}
>
{isLoading ? 'Deleting…' : 'Delete post'}
</button>
{error && <p role="alert">{error.message}</p>}
</>
)
}getItemQueryKey
The React Query key that useItem uses for one item. Use it to force a refetch from your own code.
function getItemQueryKey(id: string | undefined): readonly ['seed', 'item', string | null]Pass the same ID you gave useItem (seedLocalId, or seedUid when there is no local ID). useItem already refetches after EAS syncs and address changes, so you rarely need this.
import { useQueryClient } from '@tanstack/react-query'
import { getItemQueryKey } from '@seedprotocol/react'
function RefreshButton({ seedLocalId }: { seedLocalId: string }) {
const queryClient = useQueryClient()
return (
<button onClick={() => queryClient.invalidateQueries({ queryKey: getItemQueryKey(seedLocalId) })}>
Refresh
</button>
)
}useItems keys start with ['seed', 'items']. The rest of that key isn't public, so invalidate by the prefix: queryClient.invalidateQueries({ queryKey: ['seed', 'items'] }).
Related
- Reading and writing data: querying, relations, lists and value types
- Adding reactivity in React: what each hook subscribes to
- Item properties:
useItemProperty,useDebouncedItemPropertyand friends - useLiveQuery: SQL-level filters and sorting
- Publishing and EAS sync
Itemin the SDK:Item.create,Item.all,Item.findanddestroy- Ownership and drafts: owned and watched addresses