Item properties
Hooks for reading and editing individual property values on an item: one field, all fields, debounced text input, and creating or removing property instances.
Each model property on an item is backed by an ItemProperty instance that holds its value and saves it. The hooks on this page give components direct access to those instances, so a component can subscribe to one field instead of the whole item. For the item-level hooks, see Items.
import {
useItemProperty,
useItemProperties,
useDebouncedItemProperty,
useCreateItemProperty,
useDestroyItemProperty,
invalidateItemPropertiesForItem,
getItemPropertyQueryKey,
} from '@seedprotocol/react'useItemProperty, useDebouncedItemProperty and useItemProperties wait for the client to be ready before they query. useItemProperties needs a SeedProvider above it and throws without one. The other two work without one on a shared fallback query client and log a warning once in development builds; keep them inside SeedProvider too.
Property values are stored as text. A Boolean reads back as 'true' or 'false', a Number as a numeric string and a File as a blob: URL, so convert before comparing. An Image, File or Html whose file isn't on this device reads as undefined. See What you get back.
useItemProperty
Loads one property of one item and re-renders when its value changes.
function useItemProperty(props: { seedLocalId?: string; seedUid?: string; propertyName: string }): UseItemPropertyResult
function useItemProperty(props: { itemId?: string; propertyName: string }): UseItemPropertyResult
function useItemProperty(itemId: string, propertyName: string): UseItemPropertyResult
type UseItemPropertyResult = {
property: IItemProperty | undefined
isLoading: boolean
error: Error | null
valueStatus: 'resolved' | 'fetching' | 'missing' | undefined
}UseItemPropertyResult is a name used on this page; the package doesn't export the type.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
seedLocalId | string | The item's local ID. | |
seedUid | string | The item's EAS attestation UID. Used when seedLocalId isn't set. | |
itemId | string | Either ID. When set, it replaces both seedLocalId and seedUid. | |
propertyName | string | required | The property's name in the model, for example 'title'. |
itemId, in either the object or the two-argument form, is tried as a seedLocalId first, then as a seedUid.
Returns
| Field | Type | Description |
|---|---|---|
property | IItemProperty | undefined | The property instance. Read property.value for the value. |
isLoading | boolean | true until the lookup finishes. false when no ID or name is set. |
error | Error | null | Set when the property fails to load or times out. |
valueStatus | 'resolved' | 'fetching' | 'missing' | undefined | property.valueStatus: 'missing' when an Image, File or Html names a file that isn't on this device, so property.value is undefined; 'fetching' while the property loads; 'resolved' otherwise, and always for other types. undefined while property is. See valueStatus. |
A property that's already loaded in memory comes back on the first render with isLoading: false. When the ID or name changes, property is undefined until the new one loads.
The hook re-renders when the property's value changes, including changes made by other components or by assigning to the item (post.title = '…'). It also re-renders when only property.conflict changes, as when sync or another tab records or resolves a conflict, so a component can show property.conflict, and when the file an Image, File or Html names is found or missed, so valueStatus stays current. It doesn't re-render for changes to other properties of the same item.
To write, assign to property.value; it saves in the background, as with any item field. A text input bound to property.value keeps every keystroke. Each keystroke still starts a save, and only the newest pending one is written; for one save per pause, keep a draft and assign it on blur, or use useDebouncedItemProperty (Editing items optimistically compares the two). On an item the user doesn't own, the write fails and the property shows the stored value again (Writing), so show read-only fields for items that aren't the user's.
Example
import { useItemProperty } from '@seedprotocol/react'
function PostTitle({ seedLocalId }: { seedLocalId: string }) {
const { property, isLoading } = useItemProperty({ seedLocalId, propertyName: 'title' })
if (isLoading) return <span>…</span>
return <span>{property?.value || 'Untitled'}</span>
}A synced Image whose file hasn't been downloaded to this device reads undefined with valueStatus: 'missing', and the hook re-renders once a download or another tab saves the file:
import { useItemProperty } from '@seedprotocol/react'
function Cover({ seedLocalId }: { seedLocalId: string }) {
const { property, valueStatus } = useItemProperty({ seedLocalId, propertyName: 'cover' })
if (valueStatus === 'missing') return <p>Image not on this device yet.</p>
if (!property?.value) return null
return <img src={String(property.value)} alt="" />
}useDebouncedItemProperty
useItemProperty plus a setValue function for text inputs. setValue shows the new value at once and saves it after the input has been idle for debounceMs.
function useDebouncedItemProperty(
params:
| { seedLocalId?: string; seedUid?: string; propertyName: string }
| { itemId: string; propertyName: string },
debounceMs?: number,
): {
property: IItemProperty | undefined
setValue: (value: string) => void
flush: () => void
isLoading: boolean
error: Error | null
}Parameters
| Name | Type | Default | Description |
|---|---|---|---|
params | object | required | The same lookups as useItemProperty's object forms. |
debounceMs | number | 300 | How long to wait after the last setValue before saving. |
Returns
The same fields as useItemProperty except valueStatus (read property?.valueStatus), plus:
| Field | Type | Description |
|---|---|---|
setValue | (value: string) => void | Shows the value right away with property.showValue(), so every subscriber re-renders, and calls property.setValue() with the latest value once the input has been idle for debounceMs. Does nothing while property is undefined. |
flush | () => void | Starts the pending save now instead of after debounceMs, for example before navigating. Does nothing when no save is pending. |
The save validates the value like an assignment. A value that breaks a rule stays in the input and isn't written, the reasons are in property.saveValidationErrors, and the hook's error is an ItemPropertySaveValidationError listing them. A write that's refused or fails shows the stored value again, with the cause in error. The next save clears error.
On an item the user doesn't own, the text the user typed disappears when the debounce fires: the input shows the stored value again and error is an OwnershipError. Check isItemOwned and show a read-only field instead.
A pending save is also started when the component unmounts or debounceMs changes, so the last edit is written even if the user navigates away within the debounce window.
setValue takes a string. Use it for Text and Html fields. For Number, Boolean and other types, assign the typed value to the item or to property.value instead.
Example
import { useDebouncedItemProperty } from '@seedprotocol/react'
function TitleInput({ seedLocalId }: { seedLocalId: string }) {
const { property, setValue } = useDebouncedItemProperty({ seedLocalId, propertyName: 'title' }, 500)
return (
<input
value={String(property?.value ?? '')}
onChange={(e) => setValue(e.target.value)}
/>
)
}useItemProperties
Loads every property of an item, for generic editors that iterate fields without hard-coding names.
function useItemProperties(props: { seedLocalId?: string; seedUid?: string }): UseItemPropertiesResult
function useItemProperties(itemId: string): UseItemPropertiesResult
type UseItemPropertiesResult = {
properties: IItemProperty[]
isLoading: boolean
error: Error | null
}UseItemPropertiesResult is a name used on this page; the package doesn't export the type. The string form is looked up as a local ID.
Returns
| Field | Type | Description |
|---|---|---|
properties | IItemProperty[] | One entry per property. [] until loaded or when no ID is set. |
isLoading | boolean | true during the first fetch. |
error | Error | null | The fetch error, if any. |
The list contains:
- every property that has a stored value
- every other property the model defines, with a
nullvalue, so an editor can show empty fields - a
createdAtentry built from the item's creation time, if the model has nocreatedAtproperty of its own
The hook re-reads the item's rows in the metadata table when a write touches them, from any tab or from sync, and refetches the list once per save; each refetch re-renders the component, including one after another tab's save. In the browser it hears the writes through the SDK's change feed; on Node it polls them with a live query. It doesn't refetch when the item's last row is removed. While mounted, the hook holds the properties it returns, so an item unloading elsewhere doesn't stop them, and the list isn't cached after unmount.
The order of properties isn't defined. Sort it, or look entries up by propertyName, if order matters. Skip the createdAt entry if your editor shouldn't show it.
Example
import { useItemProperties } from '@seedprotocol/react'
function PropertyTable({ seedLocalId }: { seedLocalId: string }) {
const { properties, isLoading } = useItemProperties({ seedLocalId })
if (isLoading) return <p>Loading…</p>
const rows = [...properties]
.filter((p) => p.propertyName !== 'createdAt')
.sort((a, b) => a.propertyName.localeCompare(b.propertyName))
return (
<dl>
{rows.map((p) => (
<div key={p.propertyName}>
<dt>{p.propertyName}</dt>
<dd>{String(p.value ?? '')}</dd>
</div>
))}
</dl>
)
}useCreateItemProperty
Returns a create function that gets or creates the ItemProperty instance for one property of an item. You rarely need it: items create their property instances when they load. It's for code that builds property instances directly.
function useCreateItemProperty(): {
create: (props: UseCreateItemPropertyProps) => IItemProperty | undefined
isLoading: boolean
error: Error | null
resetError: () => void
}
type UseCreateItemPropertyProps = {
seedLocalId?: string
seedUid?: string
propertyName: string
modelName: string
propertyValue?: any
versionLocalId?: string
versionUid?: string
[key: string]: any
}The types are exported as UseCreateItemPropertyProps and UseCreateItemPropertyReturn.
create parameters
| Name | Type | Default | Description |
|---|---|---|---|
seedLocalId / seedUid | string | The item. One of them is required. | |
propertyName | string | required | The property's name in the model. |
modelName | string | required | The item's model. |
propertyValue | any | The starting value for a new instance. | |
versionLocalId / versionUid | string | The version the value belongs to. |
Other keys are passed through to ItemProperty.create().
Returns
| Field | Type | Description |
|---|---|---|
create | function | Returns the instance synchronously, or undefined if a required field is missing or creation fails (and sets error). |
isLoading | boolean | true until the returned instance finishes loading. |
error | Error | null | The missing-field error, or the instance's load error when its load fails (Failed to create item property when no instance is created or the load error isn't an Error). |
resetError | () => void | Sets error back to null. |
If an instance for that item and property is already loaded, create returns it and ignores propertyValue. A new instance lives in memory until a value is saved: assign property.value to store one. create takes no hold on it, so the instance unloads after the grace period (entityCache.unloadGraceMs in SeedConfig, 60 seconds by default) unless a useItemProperty shows it. A value assigned before then is stored; an assignment after it throws EntityUnloadedError.
Example
import { useCreateItemProperty } from '@seedprotocol/react'
function AddSubtitle({ seedLocalId }: { seedLocalId: string }) {
const { create, error } = useCreateItemProperty()
const handleClick = () => {
const property = create({ seedLocalId, modelName: 'Post', propertyName: 'summary' })
if (property) property.value = 'A short summary'
}
return (
<>
<button onClick={handleClick}>Add summary</button>
{error && <p role="alert">{error.message}</p>}
</>
)
}useDestroyItemProperty
Returns a destroy function that deletes one property instance with itemProperty.destroy().
function useDestroyItemProperty(): {
destroy: (itemProperty: IItemProperty) => Promise<void>
isLoading: boolean
error: Error | null
resetError: () => void
}The return type is exported as UseDestroyItemPropertyReturn.
It fails the same way as useDeleteItem: when destroy() fails (the item isn't the user's, or the database write fails), error is set and the promise rejects. Catch the promise.
import { useDestroyItemProperty, useItemProperty } from '@seedprotocol/react'
function ClearSummary({ seedLocalId }: { seedLocalId: string }) {
const { property } = useItemProperty({ seedLocalId, propertyName: 'summary' })
const { destroy, isLoading } = useDestroyItemProperty()
if (!property) return null
return (
<button disabled={isLoading} onClick={() => destroy(property).catch(() => {})}>
Remove summary
</button>
)
}To clear a value but keep the field, call property.clear() (or assign '') instead. On a published item, the next publish attests the removal (clear).
invalidateItemPropertiesForItem
Forces useItemProperties to refetch for one item and resolves when the refetch is done.
function invalidateItemPropertiesForItem(canonicalId: string): Promise<void>Pass the ID you gave useItemProperties: the seedLocalId, or the seedUid when you used that. It only works while a SeedProvider is mounted; otherwise it resolves without doing anything. Saves don't need it: the hook refetches when a stored value changes.
await invalidateItemPropertiesForItem(seedLocalId)getItemPropertyQueryKey
The React Query key that useItemProperty uses. Use it to force a refetch of one property from your own code.
function getItemPropertyQueryKey(
seedLocalId: string | undefined,
seedUid: string | undefined,
propertyName: string,
): readonly ['seed', 'itemProperty', string | null, string | null, string]When you looked the property up with itemId, pass that ID as seedLocalId and undefined as seedUid.
import { useQueryClient } from '@tanstack/react-query'
import { getItemPropertyQueryKey } from '@seedprotocol/react'
function useRefreshTitle(seedLocalId: string) {
const queryClient = useQueryClient()
return () =>
queryClient.invalidateQueries({
queryKey: getItemPropertyQueryKey(seedLocalId, undefined, 'title'),
})
}Related
- Items:
useItem,useItems,useCreateItem,useDeleteItem - Reading and writing data: how assignments save, validation, and value types
- Adding reactivity in React: choosing the smallest subscription
- Optimistic item editing
ItemPropertyin the SDK:value,save(),saveValidationErrors