@custral/ui gives a React app a provider, hooks over the /v1 API,
and validation derived from your workspace’s own object schema. Add a property in Custral and your
forms enforce it the next time the schema loads.
It runs in the browser with a publishable key (pk_…). For server code with a secret key, use
@custral/sdk. For the chat widget, use @custral/widget.
Coming from
@custral/ui@1.x? 1.x was the chat-widget driver, which now lives in
@custral/widget and @custral/js. 2.0 is a different package
under the same name. A ^1.0.0 range never installs it, so nothing changes until you ask for 2.x.Install
Quickstart
apiKey, not key. React reserves key, so a provider that declared it would never
receive your credential.
Keys
Pass a publishable key. A secret key (sk_…) is refused in a browser and throws, because anyone who
views the page source can read it, and rotating it is the only fix.
A publishable key can read and meter usage, and nothing else. useCreateRecord and useUpdateRecord
exist for a client holding a token your own backend minted, and a plain pk_ is refused on them.
Check what the key can do before you render a control that needs it:
A token that rotates
Apk_ is long-lived, so apiKey takes a string. A short-lived credential, such as an embed frame’s
emb_… or a bearer token your backend mints, goes in getToken instead. It is read fresh for every
request:
- A refreshed token keeps the cache. Changing
apiKeystarts an empty cache on purpose.getTokendoes not, because a refreshed token belongs to the same viewer. An embed token expires every 15 minutes, and refetching everything that often is what this avoids. - An inline arrow is safe. The getter is re-read on every render, so
getToken={() => token}sends the newest token. - Keep the viewer stable. If the token can switch to a different workspace or viewer, remount the
provider with React’s
key. Otherwise the new viewer reads the previous one’s cache.
Authorization header, so render the hooks once
you have one.
Hooks
Every read hook returns
{data, error, isLoading, isValidating, refresh}. useRecords adds records,
total, nextCursor and prevCursor.
Waiting for an id
Passnull and the hook sends no request and reports isLoading: false. A read that depends on
another waits this way:
Validation from your schema
You write no rule per field. The shape comes fromGET /v1/objects/:id, so a property added in Custral
is enforced as soon as the schema refetches.
validate(values) returns {valid, issues, errors}. errors has one message per failing key, ready to
show under a field. issues carries the same messages with a code to branch on: required, type,
format, range, cardinality, option or unknown_property.
boolean
For an update. A required property that is absent from
values is not reported, because you are not
clearing it. A required property that is present and blank still fails.boolean
Accept keys that match no property. Off by default: the API drops a field it cannot resolve, so an
unrecognised key is usually a typo whose data would be lost.
min and
max), boolean, date, time, date range order, how many values a single-value select takes, and whether
a select’s value is one of its options. A property type it does not recognise passes, rather than
blocking a write the API would accept.
validateRecord, validateField, isEmptyValue, writableProperties, resolveOption and
optionLabel are exported for use outside React.
Select options
useObject and useValidator include the options of each select, status and multi-select property,
so you can build the picker:
["opt_7Fd2yL8nR1"]. optionLabel(property, value) turns that into Qualified for display, and falls back to the raw value when options were not
loaded. Writes accept the id or the label.
- Absent
optionsmeans they were not loaded, which is what the objects list returns. Values are not checked. - Empty
optionsmeans the property has no choices. Every value is refused, as the API would refuse it.
user, relation, file and image values are never checked against options, because their ids come
from elsewhere in the workspace.
Caching
Hooks share a small cache scoped to the provider.- One request per key. Ten components asking for the same records share one request.
- Only the newest load writes. A slow answer for a filter you have already changed is dropped, not shown as the new filter’s result.
staleTime defaults to 30 seconds: how long data is reused before a newly mounted component refetches.
Set it on the provider or per hook. refresh() always refetches. A successful record write invalidates
record caches so lists refetch, without blanking the rows while that happens.
Changing apiKey starts a fresh cache, so one workspace’s records never show under another’s key.
Your own data layer
Already on TanStack Query or SWR? Skip the hooks and use the client:useCustralClient() returns the same client inside a provider. Every method throws a CustralError
subclass when a request is refused. The error classes are re-exported, so you can catch them without
installing @custral/js.