Skip to content

feat(useObserve): accept an undefined source - #164

Merged
mbret merged 2 commits into
mainfrom
claude/useobserve-undefined-support-gadi4y
Aug 5, 2026
Merged

feat(useObserve): accept an undefined source#164
mbret merged 2 commits into
mainfrom
claude/useobserve-undefined-support-gadi4y

Conversation

@mbret

@mbret mbret commented Aug 5, 2026

Copy link
Copy Markdown
Owner

useObserve did not accept a source that is directly undefined, which is a common case when the observable is not available yet (lazily created, coming from a state or a prop). Consumers had to wrap it in a factory (useObserve(() => source$, [source$])) just to express "maybe not there yet".

This turned out to be purely a typing gap: the store already handled a missing source (the () => Observable<T> | undefined factory path), so no runtime logic changed.

Changes

src/lib/binding/useObserve/useObserve.ts

  • New overloads for a literal undefined source: useObserve(undefined)data: undefined, and useObserve(undefined, { defaultValue })data: DefaultValue.
  • Widened the direct-source overloads from Observable<T> to Observable<T> | undefined, so a value typed Observable<number> | undefined (or BehaviorSubject<number> | undefined) infers T = number and yields data: number | undefined.
  • Added useObserve(source$ | undefined, { compareFn }) — options without defaultValue previously only existed on the BehaviorSubject overload, so a possibly-undefined subject had no way to pass just a compareFn.
  • Plain BehaviorSubject<T> (non-optional) keeps its data: T guarantee; those overloads stay first in the resolution order.

useStore.ts / store.ts

  • Source param types widened to accept undefined. No logic touched.

Behavior when the source is undefined

The pre-existing factory semantics, unchanged: { data: defaultValue, status: "success", observableState: "complete" }. Because non-factory deps are [source$], the store is recreated when the source appears and observation starts then.

Two points worth a maintainer decision, both left as-is here:

  1. success/complete for "not available yet" is debatable — pending/live arguably reads better for a source you are waiting on. Kept the existing behavior rather than making a silent breaking change (the existing factory test asserts success/complete).
  2. useObserve(undefined) with no other arguments resolves T to never, so data is exactly undefined. Accurate, but it means the element type cannot be pre-declared on a bare undefined call. In practice consumers hit the union case (Observable<T> | undefined).

Tests

Added to useObserve.test.tsx: type assertions for the new shapes (Observable<T> | undefined, BehaviorSubject<T> | undefined, literal undefined, undefined + defaultValue) plus runtime coverage for undefined → default value, custom defaultValue, and the undefined-then-defined transition.

Full suite (144 tests), tsc, and biome all pass.


Generated by Claude Code

claude added 2 commits August 5, 2026 12:50
The runtime already handled a factory returning `undefined`, but a source
that is directly `undefined` was not part of the public overloads. This is
a common case when the observable is not available yet (lazily created, from
a state or a prop).

`useObserve(undefined)` / `useObserve(source$ | undefined)` now typecheck and
return the default value with a `complete` observable state, then start
observing once an actual source is given (deps comparison recreates the store).

Also allows passing `compareFn` alone (without `defaultValue`) for plain
observables, which is needed for the `BehaviorSubject | undefined` case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112wLE8r3XNMqvKVSdD7Gvh
Without a source the store state is already final (`success` / `complete`),
which makes `subscribe` a no-op and `source$` unreachable. We were still
building a `NEVER` pipe chain (distinctUntilChanged + tap + share) and keeping
an open subscription on it until unmount, for a stream that can never emit.

Assign a bare `NEVER` and `Subscription.EMPTY` instead. `store.sub.unsubscribe()`
in useStore stays valid (EMPTY is already closed) and nothing reads `source$`
from outside the store, so the observable state is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112wLE8r3XNMqvKVSdD7Gvh
@mbret
mbret merged commit 138dcee into main Aug 5, 2026
2 checks passed
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.

2 participants