fireproof icon indicating copy to clipboard operation
fireproof copied to clipboard

feat: add loaded flag to React hooks

Open jchris opened this issue 2 months ago β€’ 2 comments

Summary

Add a loaded boolean property to all React hook results to distinguish between:

  • Not loaded yet: loaded=false, data is in initial/empty state
  • Loaded: loaded=true, data reflects actual query results (may be empty)

This solves a fundamental UX problem where hooks initially return empty data structures before the first fetch completes, making it impossible to distinguish between "loading" and "legitimately empty results".

Changes

Core Implementation

  • Created base HookResult interface with readonly loaded: boolean
  • Updated all hook result types to extend HookResult:
    • LiveQueryResult
    • AllDocsResult
    • ChangesResult
    • UseDocumentResultObject

Hook Implementations

  • useLiveQuery: Tracks loaded state, sets to true after first refreshRows() completes
  • useDocument: Tracks loaded state, sets to true after first refresh() completes
  • useAllDocs: Tracks loaded state, sets to true after first refreshRows() completes
  • useChanges: Tracks loaded state, sets to true after first refreshRows() completes

Testing

  • Added comprehensive test suite for loaded behavior
  • Tests validate loaded flag correctly distinguishes empty vs not-yet-loaded states
  • All existing tests continue to pass

Usage

const { rows, docs, loaded } = useLiveQuery("type", { key: "todo" });
const { doc, loaded, save } = useDocument({ title: "" });
const { docs, loaded } = useAllDocs();
const { docs, loaded } = useChanges([], {});

if (!loaded) {
  return <LoadingSpinner />;
}

// Now we know we have real data (or confirmed empty results)
return <TodoList items={docs} />;

Test Plan

  • [x] All existing tests pass
  • [x] New tests verify loaded flag starts false
  • [x] New tests verify loaded flag becomes true after data loads
  • [x] Test validates empty results with loaded=true are distinguishable from not-yet-loaded
  • [x] Tests cover all four hooks: useLiveQuery, useDocument, useAllDocs, useChanges
  • [x] TypeScript diagnostics are clean

Breaking Changes

None - this is a purely additive change. The loaded property is added to existing return types.

πŸ€– Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a readonly loaded flag to hook results so apps can distinguish "not yet loaded" vs "loaded (including empty)" across live queries, document, all-docs, and changes hooks; loaded resets when query/params/document change.
  • Tests

    • Added test suites validating loaded-flag behavior: initial false, transition to true, empty-result handling, reset-on-change scenarios, and proper teardown.

jchris avatar Oct 23 '25 14:10 jchris

The lifecycle is per query / hook, not per db, so this interface makes sense.

jchris avatar Oct 23 '25 14:10 jchris

[!NOTE]

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Hooks (useLiveQuery, useDocument, useAllDocs, useChanges) and related types now expose a readonly hydrated flag; hooks use per-request IDs to ignore stale async responses and reset hydrated when inputs change. Types updated; tests added/adjusted to validate hydration lifecycle and resets.

Changes

Cohort / File(s) Summary
Type definitions
use-fireproof/react/types.ts
Add interface HookResult { readonly hydrated: boolean }. Make LiveQueryResult an intersection with HookResult. Make AllDocsResult, ChangesResult, and UseDocumentResultObject extend HookResult. Change UseFireproof.attach public signature from AttachState to () => AttachState.
Hooks β€” hydration + request gating
use-fireproof/react/use-live-query.ts, use-fireproof/react/use-document.ts, use-fireproof/react/use-all-docs.ts, use-fireproof/react/use-changes.ts
Add local hydrated state (init false), per-request requestId refs, reset hydrated when inputs change, gate async refresh results by request id to ignore stale responses, set hydrated = true after successful refresh, and return { ...result, hydrated }.
Tests β€” hydration semantics & resets
use-fireproof/tests/use-fireproof.test.tsx, use-fireproof/tests/use-fireproof-loaded-resets.test.tsx, use-fireproof/tests/use-all-docs.test.tsx, use-fireproof/tests/*
Add suites and assertions for hydrated lifecycle and reset-on-dependency-change. Switch many tests to beforeAll/afterAll, use unique DB names, seed once, and reduce timeouts to 5000ms.
Other β€” logging / mocks / timeouts
core/svc/api/database-api.ts, core/tests/svc/full-round-trip.test.ts, use-fireproof/tests/img-file.test.tsx, use-fireproof/tests/*
Add a console shim suppressing console.log (preserve warn/error); replace verbose mocks with no-op-resolving mocks; reduce various test timeouts to 5000ms.
Cloud setup cleanup
cloud/backend/cf-d1/setup-backend-d1.ts, cloud/backend/cf-d1/setup.cloud.d1.ts
Adjust readiness detection string and add a readiness log; remove large commented-out setup blocks leaving only preset env call.
Websocket cleanup semantics
cloud/backend/base/msg-dispatcher-impl.ts
Close matching WebSocket with specific code and reason (close(4711, "closed by /close-connection")) when resetting connection.

Sequence Diagram(s)

sequenceDiagram
    participant Component
    participant Hook as Hook (useLiveQuery / useDocument / useAllDocs / useChanges)
    participant Data as DataSource

    Component->>Hook: mount / call hook
    activate Hook
    Hook-->>Component: initial render (hydrated = false)
    Component->>Hook: effect triggers refresh
    Hook->>Data: fetch request (tagged with requestId)
    activate Data
    Data-->>Hook: response (rows/docs)
    deactivate Data
    alt response matches latest requestId
        Note right of Hook `#ccf5d0`: apply results\nset hydrated = true
        Hook-->>Component: re-render with hydrated = true
    else stale response
        Note right of Hook `#fef3c7`: ignore stale response
    end
    deactivate Hook

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Files/areas to inspect closely:

  • use-fireproof/react/types.ts β€” exported type changes and UseFireproof.attach signature.
  • use-fireproof/react/use-document.ts β€” initialDoc id-change handling, request-id gating, and no-id edge cases.
  • Consistency of requestId increment/comparison and reset logic across all hooks.
  • New/modified tests β€” timing, flakiness risk, and correctness of hydration assertions.

Possibly related PRs

  • fireproof-storage/fireproof#702 β€” Overlaps changes to useLiveQuery internals and returned result shape.
  • fireproof-storage/fireproof#705 β€” Overlaps hook refresh/state-update logic for live queries and all-docs.
  • fireproof-storage/fireproof#801 β€” Related to the attach/attach-state refactor and changes to attach signature.

Suggested reviewers

  • necrodome

Pre-merge checks and finishing touches

βœ… Passed checks (3 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title 'feat: add loaded flag to React hooks' accurately describes the main change: introducing a 'loaded' flag to React hooks. It is specific, concise, and clearly summarizes the primary objective of the PR.
Docstring Coverage βœ… Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • [ ] πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • [ ] Create PR with unit tests
  • [ ] Post copyable unit tests in a comment
  • [ ] Commit unit tests in branch jchris/loaded-hooks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot] avatar Oct 23 '25 14:10 coderabbitai[bot]