Seed Protocol
@seedprotocol/react

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

NameTypeDefaultDescription
seedLocalIdstringThe item's local ID.
seedUidstringThe item's EAS attestation UID. Used when seedLocalId isn't set.
itemIdstringEither ID. When set, it replaces both seedLocalId and seedUid.
propertyNamestringrequiredThe 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

FieldTypeDescription
propertyIItemProperty | undefinedThe property instance. Read property.value for the value.
isLoadingbooleantrue until the lookup finishes. false when no ID or name is set.
errorError | nullSet when the property fails to load or times out.
valueStatus'resolved' | 'fetching' | 'missing' | undefinedproperty.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

NameTypeDefaultDescription
paramsobjectrequiredThe same lookups as useItemProperty's object forms.
debounceMsnumber300How long to wait after the last setValue before saving.

Returns

The same fields as useItemProperty except valueStatus (read property?.valueStatus), plus:

FieldTypeDescription
setValue(value: string) => voidShows 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() => voidStarts 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

FieldTypeDescription
propertiesIItemProperty[]One entry per property. [] until loaded or when no ID is set.
isLoadingbooleantrue during the first fetch.
errorError | nullThe fetch error, if any.

The list contains:

  • every property that has a stored value
  • every other property the model defines, with a null value, so an editor can show empty fields
  • a createdAt entry built from the item's creation time, if the model has no createdAt property 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

NameTypeDefaultDescription
seedLocalId / seedUidstringThe item. One of them is required.
propertyNamestringrequiredThe property's name in the model.
modelNamestringrequiredThe item's model.
propertyValueanyThe starting value for a new instance.
versionLocalId / versionUidstringThe version the value belongs to.

Other keys are passed through to ItemProperty.create().

Returns

FieldTypeDescription
createfunctionReturns the instance synchronously, or undefined if a required field is missing or creation fails (and sets error).
isLoadingbooleantrue until the returned instance finishes loading.
errorError | nullThe 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() => voidSets 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'),
    })
}

On this page