Repository navigation
feat(appkit-ui): add database React hooks with read invalidation - #609
Conversation
📦 Bundle size reportCompared against
|
| dist | raw | gzip |
|---|---|---|
| JS (runtime) | 1.2 MB (+86 B) | 447 KB (+9 B) |
| Type declarations | 456 KB | 165 KB (+2 B) |
| Source maps | 2.5 MB (+20 B) | 837 KB (+13 B) |
| Other | 11 KB | 3.7 KB |
| Total | 4.1 MB (+106 B) | 1.4 MB (+24 B) |
Per-entry composition (own code — deps external (as shipped))
| Entry | Initial (gz) | Lazy (gz) | Total (gz) | node_modules (min) | Own code (min) |
|---|---|---|---|---|---|
. |
102 KB (-1 B) | 2.5 KB (-1 B) | 105 KB (-2 B) | external | 331 KB |
./beta |
98 KB | 484 B | 99 KB | external | 297 KB |
./testing |
42 KB | 32 KB (+2 B) | 74 KB (+2 B) | external | 215 KB |
./tsdown |
520 B | 0 B | 520 B | external | 813 B |
./type-generator |
23 KB | 0 B | 23 KB | external | 66 KB |
Chunks:
| Entry | Chunk | Load | Size (gz) |
|---|---|---|---|
. |
index.js |
initial | 97 KB |
. |
utils.js |
initial | 4.6 KB |
. |
remote-tunnel-manager.js |
lazy | 2.5 KB |
./beta |
beta.js |
initial | 81 KB |
./beta |
stream-manager.js |
initial | 5.9 KB |
./beta |
service-context.js |
initial | 4.2 KB |
./beta |
databricks.js |
initial | 3.3 KB |
./beta |
wide-event-emitter.js |
initial | 3.2 KB |
./beta |
client.js |
initial | 594 B |
./beta |
index.js |
initial | 20 B |
./beta |
supervisor-api.js |
lazy | 193 B |
./beta |
databricks.js |
lazy | 176 B |
./beta |
index.js |
lazy | 115 B |
./testing |
manifest.js |
initial | 29 KB |
./testing |
index.js |
initial | 10 KB |
./testing |
wide-event-emitter.js |
initial | 2.9 KB |
./testing |
index.js |
lazy | 28 KB |
./testing |
remote-tunnel-manager.js |
lazy | 2.5 KB |
./testing |
utils.js |
lazy | 1.8 KB |
./tsdown |
index.js |
initial | 520 B |
./type-generator |
index.js |
initial | 23 KB |
@databricks/appkit-ui
npm tarball (packed): 397 KB (-4 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).
| dist | raw | gzip |
|---|---|---|
| JS (runtime) | 428 KB | 146 KB |
| Type declarations | 266 KB | 98 KB (-2 B) |
| Source maps | 852 KB | 285 KB |
| CSS | 16 KB | 3.2 KB |
| Total | 1.5 MB | 533 KB (-2 B) |
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
| Entry | Initial (gz) | Lazy (gz) | Total (gz) | node_modules (min) | Own code (min) |
|---|---|---|---|---|---|
./js |
5.3 KB | 49 KB | 55 KB | 208 KB | 14 KB |
./js/beta |
2.1 KB | 0 B | 2.1 KB | 0 B | 5.0 KB |
./react |
433 KB | 49 KB | 481 KB | 1.3 MB | 177 KB |
./react/beta |
4.9 KB | 0 B | 4.9 KB | 0 B | 12 KB |
Chunks:
| Entry | Chunk | Load | Size (gz) |
|---|---|---|---|
./js |
index.js |
initial | 5.2 KB |
./js |
chunk |
initial | 120 B |
./js |
apache-arrow |
lazy | 49 KB |
./js/beta |
beta.js |
initial | 2.1 KB |
./react |
index.js |
initial | 431 KB |
./react |
tslib |
initial | 2.1 KB |
./react |
apache-arrow |
lazy | 49 KB |
./react/beta |
beta.js |
initial | 4.9 KB |
🤖 AppKit PR bot🔬 Run evalsStart an eval for this PR from the evals-monitor app: Go to Evals Monitor → 📦 Try this PR's app templateScaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh run download 37697602718 -R databricks/appkit -n appkit-template-0.86.0-pr.95ff9d5-stack-database-hooks-02-react-hooks-609 -D appkit-pr-609 \
&& unzip -o "appkit-pr-609/appkit-template-0.86.0-pr.95ff9d5-stack-database-hooks-02-react-hooks-609.zip" -d "appkit-pr-609" \
&& databricks apps init --template "appkit-pr-609"The template pins |
1048b8f to
42a4cd6
Compare
476b24e to
a29a2e1
Compare
atilafassina
left a comment
There was a problem hiding this comment.
Re-reviewed at a29a2e1.
Fixed since the first pass:
- Bundle baseline is regenerated (e010f53, a29a2e1).
- A write now follows superseding refreshes instead of resolving when another refresh aborts its reads, with tests for overlapping
restartStartedand two concurrent write hooks.
Hooks and database client tests pass locally (301).
Accepted as-is, worth follow-ups:
- Default
invalidate: truestill restarts every mounted read per write. Consider defaulting to the written entity plus its relations. - Coalesce overlapping invalidations to bound W×R refetches.
- Sync
optionsRefin a layout effect so a changedinvalidate/onSuccessis not missed by a write that settles first. - Public
clearDatabaseReads()for logout / account switch, plus a docs note. - Catch
URIErrorfromencodeURIComponenton lone surrogates and throwINVALID_REQUEST. encodeURIComponent(String(boardId))in the docs example near database.md:597.
a29a2e1 to
7dfc968
Compare
Add two beta read hooks in @databricks/appkit-ui/react/beta on top of the databaseApi transport. Reads share one request per resolved URL through the existing request store, so identical reads across components, re-renders with equal params, and StrictMode remounts reuse one fetch; the last subscriber's release aborts it. Params keep the exactness check from databaseApi, so private columns, undeclared keys, keyed reads on keyless or private-key tables, and to-one include limits stay compile errors. `serialized<T>()` lets a serializer-shaped read declare its row type without losing entity and params checking. The playground's BoardExplorer now reads boards, notes, the timeline, and a note's full body through the hooks; its writes stay manual until the write hooks land. The database plugin docs gain a React hooks section. Co-authored-by: Isaac <no-reply@databricks.com> Signed-off-by: ditadi <victordperd@gmail.com>
Add three beta hooks: useDatabaseCreate, useDatabaseUpdate, and useDatabaseDelete. Like useServingInvoke, a hook call never rejects: it resolves with the row (or `true` for a delete), or with `null`/`false` once the DatabaseApiError is in `error`, so handlers need no try/catch. databaseApi remains the layer that throws. A successful write restarts every mounted database read by default, since includes are invisible at runtime; `invalidate` narrows it to named entities or turns it off. `invalidateDatabaseReads` is exported so writes made through databaseApi or custom routes can refresh hook reads too. Writes are never aborted, only the latest call reports its state, and a write that succeeds after unmount still restarts reads. The generic request store gains `restartStarted(match?)`. The playground's BoardExplorer now writes through the hooks and refreshes without manual refetches. Co-authored-by: Isaac <no-reply@databricks.com> Signed-off-by: ditadi <victordperd@gmail.com>
Signed-off-by: ditadi <victordperd@gmail.com>
…ished relations Signed-off-by: ditadi <victordperd@gmail.com>
Signed-off-by: ditadi <victordperd@gmail.com>
Record the measured growth of the React database hooks over the client baseline. The react/beta consumer entry grows by about 3.8 KiB gzip with no bundled dependencies. Generated from a clean build. The 5% and 10 KiB budget gates are unchanged. Signed-off-by: ditadi <victordperd@gmail.com>
A write must not resolve when another refresh aborts its original reads. Track the latest run per retained entry and recheck the entire target set after settlement, including keys that completed before a later restart. Cover overlapping restartStarted calls and two concurrent write hooks, plus manual refetch, reentrant starts, re-retain, and entry replacement. Regenerate the measured bundle baseline from a clean build. Signed-off-by: ditadi <victordperd@gmail.com>
Page, DatabaseReadScope, and RetainOptions only have file-local consumers. Remove their unused exports rather than suppressing the Knip check. Regenerate the bundle baseline after rebasing onto the v0.86 main branch. Signed-off-by: ditadi <victordperd@gmail.com>
7dfc968 to
9ca4fe4
Compare
Why the change
With
databaseApiin place, React code still had to track loading and error state, avoid duplicate requests, and reload data after every write. This adds read and write hooks (beta) that share requests, keep error state stable, and refresh the reads on screen after a successful write.Special things to note
invalidate: ["notes"]narrows the restart to reads that shownotesrows, including reads that reachnotesthrough aninclude.falseturns it off.DatabasePlugin.clientConfig()returns{ relations: { table: { relation: targetTable } } }for exposed tables only. These are the namesincludealready accepts and the generated types already carry. This is the one change inpackages/appkit.useServingInvoke: a failure resolvesnull(orfalsefor a delete) and the error is inerror. They never abort a request on unmount, because aborting wouldn't undo a committed write. A successful call resolves, and leavesloading, only once the reads it restarted have reloaded.databaseApistays the layer that throws."",".", or".."withINVALID_REQUESTand send nothing. URL resolution would otherwise send the request to the list route or the plugin root. The check is inresolveDatabaseUrl, sodatabaseApiis covered too.bundle-size-baseline.jsonis regenerated. Theappkit-uitarball grows from 358 KB to 404 KB across the feat(appkit-ui): add typed databaseApi client for DatabasePlugin routes #608 + feat(appkit-ui): add database React hooks with read invalidation #609 stack, which is over the 5% budget. The increase is the new client and hooks: no dependency is added, and no test file ships.Change outline
The new public surface, typed by the same generated registry as
databaseApi:Types follow the existing hook naming:
UseDatabaseListOptions/UseDatabaseListResult,UseDatabaseCreateOptions/UseDatabaseCreateResult, and so on.Read behavior:
nullparams hold a list idle, for params that depend on another read.keepPreviousData: truekeeps the previous page visible while the next one loads.shapetakesserialized<T>()to only declare the row type, or a function such as zod'sparseto check every row at runtime. A row that fails its shape fails the read withINTERNAL; the schema error stays incause, since it can quote row values.datavisible while it loads and after it fails. The exception isNOT_FOUND: the row is gone, sodatais cleared.errorkeeps its identity across renders, so an effect keyed on it, such as a toast, runs once.Write behavior:
onSuccessandonErrorrun for every call, including a superseded call and one that finishes after unmount. Inline callbacks don't change the identity ofcreate,update, orremove.How a read and a write move through the layers:
Reads reuse the existing
createRequestStore. It gains per-entry metadata and a way to restart the reads that are already running, which resolves once they settle:interface RequestStore<S, M> { - retain(key, run, autoStart?): () => void; + retain(key, run, options?: { autoStart?, meta?: M }): () => void; start(key): void; + restartStarted(match?: (key, meta?: M) => boolean): Promise<void>; subscribe(key, listener): () => void; getSnapshot(key): S; }A runner may return a promise that settles when its run does.
restartStartedskips entries whose last subscriber already left.useAnalyticsQuerypasses{ autoStart }and is otherwise unchanged.databaseApichanges in two ways. It exports its transport helpers, marked@internal:resolveDatabaseUrl,requestDatabase, and the untyped row writers. It also gains the id check described above.The playground's
BoardExplorerdrops its last hand-written request:The database plugin docs gain a "React hooks (beta)" section: lists, pagination, records, serializer shapes, writes, callbacks, invalidation, and lifecycles.
The PR has four commits: read hooks, write hooks, strict query types, and the hardening above. The hook tests share
database-test-utils.ts.This pull request and its description were written by Isaac.