Skip to content

Conversation

@phaux
Copy link
Contributor

@phaux phaux commented Oct 4, 2025

Implemented suspending hook similar to #1276 (comment)

Right now it never clears the caches. I couldn't think of a simple way to implement it.

I had in mind the latest changes in Observable spec which say that observables are always hot (shared) and there is no way to detect new subscriber from observable side. This means that it's impossible to implement a BehaviorSubject which emits last value for each new subscriber. That's why I'm keeping the last value in memory for each observable. Even this is not perfect because if you pass an observable to the hook and also use it somewhere else then the first value could be missed.

I will upload an example app later which uses this so you can test it.

Summary by CodeRabbit

  • New Features

    • Added suspending hooks for live queries and observables, enabling seamless React Suspense integration
    • Introduced promise utility for cleaner async data handling
  • Chores

    • Updated React and TypeScript type definitions to React 19 with ES2023 support
    • Enhanced test infrastructure with modern React APIs

@phaux
Copy link
Contributor Author

phaux commented Oct 7, 2025

Example vibe coded app which uses this hook: https://stackblitz.com/~/github.com/phaux/rrdex

Notice how if you replace this hook with regular useLiveQuery then an empty/not found state will appear briefly sometimes on navigation and also entry animations will play even though they shouldn't on first render (they should only play when adding new element to a list).

If you also remove remountOnParamsChange in main then an exit animation followed by an entry animation plays for all elements in a list on every navigation, which looks cool but is not desired in this case.

I used my own list animation component in this example, but the problem also appears when using other FLIP animations libraries or React's experimental ViewTransition component.

@phaux phaux marked this pull request as ready for review October 7, 2025 23:06
@dfahlander
Copy link
Collaborator

dfahlander commented Oct 8, 2025

Thank you for this great piece of code! I looked through the code and it looks really thought-through, intelligent and clear. I will defenitely merge this for a new official release of dexie-react-hooks. Need some small things fixed first though:

  • update react in devDeps with pnpm so we get rid of some build errors of the typings
  • support for synchronous observables that resolve immediately
  • update tsconfig to support iterators and remove the Array.from() as you've commented in the code already (we shouldn't target es5 anymore! should change to ES2016)
  • support for React < 19 (would need to throw the promise if still pending, instead of using React.use())
  • support for observables and not only observable factories

@phaux phaux force-pushed the use-suspending-live-query branch from 9307bbe to c3f501f Compare October 8, 2025 14:26
@phaux
Copy link
Contributor Author

phaux commented Oct 8, 2025

Sorry but I don't know how to run tests in this repo. I only get SyntaxError: Unexpected identifier 'assert' when trying to build y-dexie.

I did everything except "optimization for synchronous observables" because I don't think it's necessary? A BehaviorSubject from rxjs will already emit synchronously and the next callback will run instantly so there will be no suspense.

Now I want to add some tests but I'm gonna need help with that.

@dfahlander
Copy link
Collaborator

dfahlander commented Oct 8, 2025

I see. You need to have pnpm installed and then cd to the repo root and

pnpm install
pnpm build
pnpm test
cd libs/dexie-react-hooks
pnpm install
pnpm build 
pnpm test

If we need to discuss more please join discord server https://discord.gg/huhre7MHBF

@phaux
Copy link
Contributor Author

phaux commented Oct 9, 2025

Running tests in root fails with Cannot find plugin "karma-webdriver-launcher".

Building libs/dexie-react-hooks fails with TS2307: Cannot find module 'y-dexie' or its corresponding type declarations.

Building addons/y-dexie fails with SyntaxError: Unexpected identifier 'assert'

@dfahlander
Copy link
Collaborator

Running tests in root fails with Cannot find plugin "karma-webdriver-launcher".

Building libs/dexie-react-hooks fails with TS2307: Cannot find module 'y-dexie' or its corresponding type declarations.

Building addons/y-dexie fails with SyntaxError: Unexpected identifier 'assert'

Oh, y-dexie is a new dev-dependency. Need to pnpm install and build it too (addons/y-dexie). But with pnpm in the root having done pnpm install, pnpm build, pnpm test should work in the root.

I just cloned the repo on a fresh location to verify. Here are my commands just now that at least work on my macbook with pnpm 9.8.0 installed.

git clone https://github.com/dexie/Dexie.js dexie4
cd dexie4
pnpm install
pnpm build
pnpm test
cd addons/y-dexie
pnpm install
pnpm build
cd ../../libs/dexie-react-hooks/
pnpm install
pnpm build
pnpm test

@phaux
Copy link
Contributor Author

phaux commented Oct 9, 2025

I added some tests. I simply modified the test app to use the suspending hook in one place and added additional steps and checks.

It would be nice to also check if it works with <Suspense> by checking if it's rendering the fallback and also with useTransition by checking if it's rendering with isPending equal true.

Also a check if the caches are cleared after a query is no longer in use would make the tests better.

I tried to do these but that's not so easy. Maybe I will try again to come up with something later.

@dfahlander
Copy link
Collaborator

I added support for synchronous observables and for providing the observable itself instead of a function returning an observable. Maybe not super useful for suspense case, but in case people would prefer useSuspenseObservable() to ensure not getting undefined back for these cases, it's nice that it works equally well.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks 👍

@ryanb
Copy link

ryanb commented Oct 21, 2025

@phaux Thanks for this PR! I'm very much looking forward to using Dexie with Suspense.

I'm trying out this PR and ran into an issue. A call to useSuspendingLiveQuery is returning results from another query.

Looking at the code. The dependencies passed to useSuspendingObservable are used as the cache key. So if two different queries happen to use the same dependencies then they will use the same observable. For example.

const category = useSuspendingLiveQuery(() => db.categories.get(categoryId), [categoryId])
const products = useSuspendingLiveQuery(() => db.products.where({ categoryId }).toArray(), [categoryId])

Here products could be set to category since they share the same dependencies.

Update: Looking at the example app, a string is given as the first dependency to useSuspendingLiveQuery, is this to handle this global cache issue? I think passing a unique string to each query will be difficult to remember and has potential to conflict unintentionally elsewhere in the code base. When not done correctly it can cause some very tricky bugs.

@dfahlander
Copy link
Collaborator

Update: Looking at the example app, a string is given as the first dependency to useSuspendingLiveQuery, is this to handle this global cache issue? I think passing a unique string to each query will be difficult to remember and has potential to conflict unintentionally elsewhere in the code base. When not done correctly it can cause some very tricky bugs.

Yes, that could be a real issue. Maybe the unique string could be enforced to be used as the first parameter, before the callback. That might make it harder to miss it?

@dfahlander
Copy link
Collaborator

dfahlander commented Oct 22, 2025

export const useGetCathegoryById = (categoryId: string) => useSuspendingLiveQuery(
  "useGetCathegoryById",
  () => db.categories.get(categoryId),
  [categoryId]
)

@ryanb
Copy link

ryanb commented Oct 22, 2025

It's unfortunate there's no way to handle this internally so useSuspendingLiveQuery can have the same interface as useLiveQuery.

I played around with useId and useMemo but neither one persists on the initial re-render through Suspense.

As a "quick fix" for now I'm doing querier.toString() which isn't perfect since duplicate function strings don't necessarily mean the behavior is the same. However it's good enough for my needs.

@phaux
Copy link
Contributor Author

phaux commented Oct 22, 2025

Maybe a better API would be something like

useSuspendingObservable(db, "tableName", (table) => table.get(itemId), [itemId])

So the user is forced to give the table name as string and it will be used in the key automatically.

@ryanb
Copy link

ryanb commented Oct 22, 2025

@phaux I don't know if the table name is unique enough. For example.

const productsWithCategory = useSuspendingLiveQuery(db, "products", (table) => table.where({ categoryId }).toArray(), [categoryId])
const productsWithTag = useSuspendingLiveQuery(db, "products", (table) => table.where({ tagId }).toArray(), [tagId])

If both categoryId and tagId happen to be the same then it will share the cache.

@coderabbitai
Copy link

coderabbitai bot commented Nov 1, 2025

Walkthrough

The pull request upgrades dexie-react-hooks to support React 19, introduces two new React hooks for suspending components until observable values are available (useSuspendingLiveQuery and useSuspendingObservable), adds TypeScript type declarations for y-dexie and yjs, implements a usePromise utility, and updates test infrastructure to React 18 APIs.

Changes

Cohort / File(s) Summary
Package Configuration
addons/y-dexie/package.json
Changed "require" export from false to null for main entry point
React 19 Upgrade & Dependencies
libs/dexie-react-hooks/package.json
Bumped version to 4.2.1-alpha.1; upgraded React and @types/react from 17 to 19; aligned Dexie workspace dependencies to >=4.2.0-alpha.1 <5.0.0
TypeScript Configuration
libs/dexie-react-hooks/tsconfig.json
Updated lib from es2021 to es2023; changed moduleResolution to node16; updated target to es2022; changed module to node16
Ambient Type Declarations
libs/dexie-react-hooks/src/types/y-dexie.d.ts, libs/dexie-react-hooks/src/types/yjs.d.ts
Added ambient module declarations for y-dexie (YDoc, DexieYProvider, YUpdateRow, YSyncState, compressYDocs, yDexie default export) and yjs (minimal Doc type)
New Hook Implementations
libs/dexie-react-hooks/src/usePromise.ts
Implemented usePromise hook with React.use fallback and WeakMap-based promise state tracking for suspension support
New Suspending Hooks
libs/dexie-react-hooks/src/useSuspendingLiveQuery.ts, libs/dexie-react-hooks/src/useSuspendingObservable.ts
Added useSuspendingLiveQuery and useSuspendingObservable hooks with observable caching, subscription management, and promise-based suspension
Observable Type Refinement
libs/dexie-react-hooks/src/useObservable.ts
Extracted AnySubscription type alias and updated InteropableObservable.subscribe return type
Main Exports
libs/dexie-react-hooks/src/dexie-react-hooks.ts
Added re-exports for useSuspendingLiveQuery and useSuspendingObservable
Error Boundary Update
libs/dexie-react-hooks/test/components/ErrorBoundrary.tsx
Updated to React 19 with explicit PropsWithChildren, constructor typing, and componentDidCatch error info typing
Component Hook Migration
libs/dexie-react-hooks/test/components/ItemLoaderComponent.tsx
Replaced useLiveQuery with useSuspendingLiveQuery; removed explicit loading state; updated dependency key
Test Setup & Flow
libs/dexie-react-hooks/test/index.ts
Migrated to React 18 createRoot API; added synchronous DOM waits; extended test flow for item navigation and error recovery verification

Sequence Diagram(s)

sequenceDiagram
    participant Component
    participant useSuspendingObservable
    participant Observable
    participant Cache
    participant usePromise
    participant React

    Component->>useSuspendingObservable: Call hook with observable
    useSuspendingObservable->>Cache: Check cached value
    alt Value cached
        Cache-->>useSuspendingObservable: Return cached value
        useSuspendingObservable-->>Component: Return value (sync)
    else Value not cached
        useSuspendingObservable->>Observable: Subscribe to observable
        Observable->>Cache: Emit initial/next value
        useSuspendingObservable->>usePromise: Wrap pending promise
        usePromise->>React: Throw promise (Suspend)
        React->>Component: Pause rendering
        Observable-->>useSuspendingObservable: Value emitted
        usePromise-->>React: Promise resolves
        React->>Component: Resume & rerender
        useSuspendingObservable-->>Component: Return latest value
    end
    Component->>Component: Render with value
    rect rgba(100, 150, 200, 0.1)
        Note over useSuspendingObservable: On unmount: unsubscribe & schedule cache cleanup
    end
Loading
sequenceDiagram
    participant Component as Component<br/>(using useSuspendingLiveQuery)
    participant useSuspendingLiveQuery
    participant Dexie
    participant useSuspendingObservable

    Component->>useSuspendingLiveQuery: Call with querier fn & cacheKey
    useSuspendingLiveQuery->>Dexie: liveQuery(querier)
    Dexie-->>useSuspendingLiveQuery: Observable of query results
    useSuspendingLiveQuery->>useSuspendingObservable: Pass observable + prefixed cacheKey
    useSuspendingObservable-->>useSuspendingLiveQuery: Suspends until value
    useSuspendingLiveQuery-->>Component: Returns T (or throws/suspends)
    rect rgba(100, 200, 100, 0.1)
        Note over Component: Component suspends while data loads
        Note over Dexie: Dexie query executes reactively
        Note over useSuspendingObservable: Cache reused across mount cycles
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Areas requiring extra attention:

  • useSuspendingObservable.ts — Complex subscription lifecycle management, cache expiration via timeouts, handling both function and object-based unsubscribe patterns, and WeakMap-based state tracking
  • usePromise.ts — Promise state machine using WeakMap; fallback implementation mimics React.use behavior and requires careful review of state transitions and closure semantics
  • Test setup migration (test/index.ts) — React 17 to React 18 API change (ReactDOM.render to createRoot); new synchronous waits and extended test flows need verification against intended behavior
  • Type declarations (y-dexie.d.ts, yjs.d.ts) — Ambient module declarations should be validated for completeness and compatibility with actual library exports
  • React 19 upgrade (package.json) — Verify that all hook implementations and test components are compatible with React 19 APIs and deprecations

Poem

🐰 With hooks that suspend and observables dance,
We caught React's next stride in a React-ful trance!
Dexie's live queries now float in the air,
While caches and promises manage with care.

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 pull request title "feat: useSuspendingLiveQuery" directly corresponds to the primary objective of implementing a new suspending React hook, as confirmed in the PR objectives. The title clearly identifies the main feature being added and is concise and specific enough for a teammate scanning history to understand the core change. While the changeset includes supporting infrastructure like useSuspendingObservable, usePromise utilities, and type definitions, these are appropriately treated as implementation details rather than separate features worthy of the title.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% 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

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 048334a and 8cb7556.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • addons/y-dexie/package.json (1 hunks)
  • libs/dexie-react-hooks/package.json (2 hunks)
  • libs/dexie-react-hooks/src/dexie-react-hooks.ts (1 hunks)
  • libs/dexie-react-hooks/src/types/y-dexie.d.ts (1 hunks)
  • libs/dexie-react-hooks/src/types/yjs.d.ts (1 hunks)
  • libs/dexie-react-hooks/src/useObservable.ts (1 hunks)
  • libs/dexie-react-hooks/src/usePromise.ts (1 hunks)
  • libs/dexie-react-hooks/src/useSuspendingLiveQuery.ts (1 hunks)
  • libs/dexie-react-hooks/src/useSuspendingObservable.ts (1 hunks)
  • libs/dexie-react-hooks/test/components/ErrorBoundrary.tsx (2 hunks)
  • libs/dexie-react-hooks/test/components/ItemLoaderComponent.tsx (2 hunks)
  • libs/dexie-react-hooks/test/index.ts (5 hunks)
  • libs/dexie-react-hooks/tsconfig.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
libs/dexie-react-hooks/src/useSuspendingObservable.ts (2)
libs/dexie-react-hooks/src/useObservable.ts (2)
  • InteropableObservable (2-9)
  • AnySubscription (11-11)
libs/dexie-react-hooks/src/usePromise.ts (1)
  • usePromise (4-5)
libs/dexie-react-hooks/src/useSuspendingLiveQuery.ts (1)
libs/dexie-react-hooks/src/useSuspendingObservable.ts (1)
  • useSuspendingObservable (12-127)
libs/dexie-react-hooks/src/types/y-dexie.d.ts (1)
addons/y-dexie/src/y-dexie.ts (1)
  • yDexie (16-30)
libs/dexie-react-hooks/test/index.ts (3)
libs/dexie-react-hooks/test/components/App.tsx (1)
  • App (7-36)
libs/dexie-react-hooks/test/utils/waitTilEqual.ts (1)
  • waitTilEqual (3-24)
libs/dexie-react-hooks/test/db/index.ts (1)
  • db (15-15)
libs/dexie-react-hooks/test/components/ItemLoaderComponent.tsx (1)
libs/dexie-react-hooks/src/useSuspendingLiveQuery.ts (1)
  • useSuspendingLiveQuery (9-17)
🪛 Biome (2.1.2)
libs/dexie-react-hooks/src/useSuspendingObservable.ts

[error] 100-100: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🔇 Additional comments (11)
addons/y-dexie/package.json (1)

11-11: LGTM – More explicit CommonJS exclusion.

Changing require from false to null is a clearer signal that CommonJS imports are not supported for this ESM-only package.

libs/dexie-react-hooks/src/types/yjs.d.ts (1)

1-7: LGTM – Minimal ambient declaration.

This lightweight ambient declaration provides the necessary typing for the yjs module without requiring the full type definitions as a dependency.

libs/dexie-react-hooks/package.json (2)

3-3: LGTM – Alpha version for pre-release testing.

The version bump to 4.2.1-alpha.1 correctly signals this is a pre-release version.


66-72: LGTM – React 19 upgrade aligns with PR objectives.

The upgrade to React 19 and corresponding type definitions is consistent with the PR's goal to add React 19 support and new suspending hooks.

libs/dexie-react-hooks/src/usePromise.ts (3)

1-5: LGTM – Graceful React 19 fallback.

The hook correctly uses React.use when available (React 19+) and falls back to a custom implementation for earlier versions.


34-37: LGTM – WeakMap prevents memory leaks.

Using a WeakMap ensures promise state is garbage-collected when the promise is no longer referenced.


8-32: Implementation correctly uses React's Suspense pattern for promise settlement.

The fallback implementation is correct. React's Suspense automatically re-renders the component when the thrown promise settles. On the subsequent render, fallbackUsePromise retrieves the settled state from the WeakMap and returns the value or throws the reason as expected.

The async state updates in the .then() callbacks (lines 14-19) update the WeakMap cache; React's Suspense mechanism, not a state update, triggers the re-render. This is the standard pattern for Suspense-based promise handling.

libs/dexie-react-hooks/src/types/y-dexie.d.ts (1)

1-28: LGTM – Minimal ambient declarations for type resolution.

This ambient declaration provides sufficient typing for the y-dexie module. The use of any for YUpdateRow and YSyncState (lines 22-23) is acceptable for minimal ambient declarations where the full type definitions are not needed.

Note: The comment mentions this is a "quickfix" for ESM/CJS resolution issues. Consider documenting this approach in case future improvements are needed.

libs/dexie-react-hooks/src/dexie-react-hooks.ts (1)

4-6: LGTM – New suspending hooks exposed in public API.

The new exports for useSuspendingLiveQuery and useSuspendingObservable correctly expose the main features of this PR.

libs/dexie-react-hooks/src/useObservable.ts (1)

6-12: LGTM – Type alias improves readability.

Extracting the subscription return type into AnySubscription improves code clarity and makes the subscription pattern more explicit. The type correctly supports both RxJS-style subscriptions (object with unsubscribe()) and function-style subscriptions.

libs/dexie-react-hooks/tsconfig.json (1)

3-8: Verify breaking changes and browser compatibility with library's stated support policy.

The tsconfig.json now explicitly targets ES2022 with lib set to es2023. ES2022 requires Chrome 94+, Edge 94+, Firefox 93+, and Safari 16.4+. This change should be:

  1. Communicated as a breaking change if the library previously supported older browsers
  2. Verified against any stated browser support policy (not found in package.json or README)
  3. Documented in release notes or CHANGELOG (no CHANGELOG entry found)

Note: The prior tsconfig only inherited from a non-existent base config, so the exact previous target cannot be confirmed.

Comment on lines +99 to +101
if (!VALUES.has(observable)) {
usePromise(promise);
}
Copy link

@coderabbitai coderabbitai bot Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Call usePromise consistently to avoid breaking hook order.

On React 19, usePromise resolves to React.use, so it must be called every render in the same order. The current conditional means the first render without a cached value calls the hook, but once VALUES is populated subsequent renders skip it, triggering React’s “rendered fewer hooks than expected” error. Please invoke usePromise(promise) unconditionally so the hook order stays stable while still relying on the resolved promise to short‑circuit suspense.

-  if (!VALUES.has(observable)) {
-    usePromise(promise);
-  }
+  usePromise(promise);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!VALUES.has(observable)) {
usePromise(promise);
}
usePromise(promise);
🧰 Tools
🪛 Biome (2.1.2)

[error] 100-100: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
In libs/dexie-react-hooks/src/useSuspendingObservable.ts around lines 99-101,
the call to usePromise is currently wrapped in a conditional which causes hook
order to change across renders; call usePromise(promise) unconditionally every
render (remove the if) so the hook runs in the same order, while still relying
on VALUES to determine whether to use the resolved value or short-circuit
suspense.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Call usePromise consistently to avoid breaking hook order.

On React 19, usePromise resolves to React.use, so it must be called every render in the same order. The current conditional means the first render without a cached value calls the hook, but once VALUES is populated subsequent renders skip it, triggering React’s “rendered fewer hooks than expected” error. Please invoke usePromise(promise) unconditionally so the hook order stays stable while still relying on the resolved promise to short‑circuit suspense.

-  if (!VALUES.has(observable)) {
-    usePromise(promise);
-  }
+  usePromise(promise);

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

🧰 Tools
🪛 Biome (2.1.2)
[error] 100-100: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order. See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents

Not true. React use is not a hook, it's allowed in conditional paths.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🌐 Web query:

React.use conditional calls rules of hooks

💡 Result:

Short answer

  • Don’t call Hooks conditionally — always call them at the top level of a function component or a custom Hook. React relies on the call order to associate Hook state across renders. [1][2]

Why

  • If you call a Hook inside a condition/loop/nested fn, the number and order of Hook calls can change between renders, which breaks React’s internal state mapping and causes bugs or the “Hooks can only be called…” error. [1][2]

How to handle conditional behavior

  • Keep Hook calls unconditional, put the condition inside the Hook callback:
    • Bad: if (cond) useEffect(...);
    • Good: useEffect(() => { if (!cond) return; /* do work */ }, [cond]); [1]
  • Or wrap logic in a custom Hook that always runs (but internally short-circuits). [1]
  • Use the official linter (eslint-plugin-react-hooks) to catch mistakes. [1][3]

References

  • React Rules of Hooks. [1]
  • Invalid Hook Call / Rules of Hooks guide. [2]
  • Example explanation / community answers. [3]

🌐 Web query:

React.use API can be called conditionally

💡 Result:

No — you must not call React Hooks (e.g. useState, useEffect, custom hooks) conditionally. Hooks must be called unconditionally at the top level of a function component or custom hook so they run in the same order on every render; otherwise React can’t match hook calls to stored state and you get bugs or the invalid-hook-call error. Use a conditional inside the hook (e.g. inside useEffect/useMemo) or render different components / custom hooks instead. [1][2]

Sources:
[1] React — Rules of Hooks.
[2] React (legacy docs) — Rules of Hooks.


🌐 Web query:

React.use API React 19 documentation

💡 Result:

React 19 introduced the new React.use API (commonly written import { use } from "react") — a render-time API that can synchronously read:

  • a Thenable/Promise (causes the component to suspend until it resolves) and
  • a React Context value (an alternative to useContext).
    Unlike ordinary hooks, use may be called conditionally or in loops (but must be called from a component or a hook) and is designed to integrate with Suspense and streaming/server features. Note: promises created directly in render are not supported — use must consume Suspense-compatible/cached resources or promises provided by a Suspense-aware library/framework. [1][2][3]

References:

  • React 19 blog & feature overview. [1]
  • React.use coverage in community docs/articles (examples, caveats on promises-in-render). [2][3]

@dfahlander You're absolutely correct, and I apologize for the incorrect review comment. React.use is a special API introduced in React 19 that, unlike traditional hooks, can be called conditionally or in loops. The conditional call to usePromise (which wraps React.use) is perfectly valid here.

The Biome linter warning appears to be a false positive—it's treating usePromise as a traditional hook when React.use is explicitly designed to be more flexible. The conditional pattern if (!VALUES.has(observable)) { usePromise(promise); } is correct and leverages the fact that React.use doesn't follow the standard Rules of Hooks.

Thank you for the correction!


✏️ Learnings added
Learnt from: dfahlander
Repo: dexie/Dexie.js PR: 2205
File: libs/dexie-react-hooks/src/useSuspendingObservable.ts:99-101
Timestamp: 2025-11-01T15:56:12.570Z
Learning: In React 19, React.use is a special render-time API that can be called conditionally or in loops, unlike traditional hooks (useState, useEffect, etc.). It's designed to read Promises/Thenables and Context values, and does not need to follow the Rules of Hooks regarding unconditional calls at the top level.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@dfahlander
Copy link
Collaborator

I pubilshed this version of dexie-react-hooks as [email protected] so it's easier for anyone to try the current API out.

NOTE: The API might change in future versions

@phaux
Copy link
Contributor Author

phaux commented Nov 5, 2025

I've been using this hook for some time and it works pretty well. I actually ended up using just this hook modified to work on top of vanilla IndexedDB+BroadcastChannel (without Dexie). I might publish it as a separate library in the future.

I was planning to push some more tests but I couldn't make them work. I can always submit what I have as a separate WIP PR if you want to merge this as is @dfahlander .

@dfahlander
Copy link
Collaborator

dfahlander commented Nov 5, 2025

I've been using this hook for some time and it works pretty well. I actually ended up using just this hook modified to work on top of vanilla IndexedDB+BroadcastChannel (without Dexie). I might publish it as a separate library in the future.

I was planning to push some more tests but I couldn't make them work. I can always submit what I have as a separate WIP PR if you want to merge this as is @dfahlander .

I think it's fine as it is. I'll merge it to master once I get some feedback from people trying the the published alpha version of dexie-react-hooks (mainly for regression testing as we've updated tsconfig etc).

I've also laborated with an alternative API for suspense by letting a hook provide a "synchronous" API to dexie. I'll see what comes out of all this, but it's very nice that we have an npm-published version of this for people to try out!

Thanks a lot for all your effort and for a really clean and nice implementation!

@dfahlander dfahlander merged commit ff94ccd into dexie:master Nov 28, 2025
7 checks passed
resolve(val);
VALUES.set(observable, val);
if (!sub) calledSynchronously = true;
else unsub(sub);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@phaux @dfahlander I've noticed an issue with useSuspendingLiveQuery where the querier function is executed twice even though the dependencies and underlying data doesn't change.

Upon further investigation, calling subscribe on an observable appears to re-executes the query. Is this correct? I'm surprised the observable doesn't return the existing value when re-subscribing.

Since we subscribe both here and in the useEffect block below this is causing the querier to execute twice, once for each subscribe call.

I want to verify my assumptions here before creating an issue for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems correct. it's something that could be improved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants