| name | react-rxjs-observables |
| description | Use RxJS observables in React components with the use$ hook from applesauce-react |
| license | MIT |
| compatibility | opencode |
| metadata | {"framework":"react","library":"applesauce","audience":"developers"} |
Using RxJS Observables in React Components
This skill teaches you how to integrate RxJS observables into React components using the use$ hook from applesauce-react.
What I do
- Show you how to subscribe to observables and automatically manage their lifecycle
- Explain the factory function pattern with dependencies for reactive data
- Cover common patterns like chained observables, side effects, and conditional subscriptions
- Help you avoid common mistakes with dependency arrays and re-subscriptions
- Guide you through loading states, error handling, and performance optimization
When to use me
Use this skill when you need to:
- Subscribe to RxJS observables in React components
- Work with Applesauce models (ProfileModel, ThreadModel, CommentsModel, etc.)
- Access reactive properties from casts like
note.author.profile$, user.contacts$, etc.
- Set up relay subscriptions or event loaders
- Combine multiple observables with
combineLatest, switchMap, or other RxJS operators
- Debug infinite re-subscription loops or stale data issues
Core Hook: use$
Import from your hooks directory:
import { use$ } from "@/hooks/use$";
Type Signatures
use$<T>(observable?: BehaviorSubject<T>): T
use$<T>(observable?: Observable<T>): T | undefined
use$<T>(factory: () => Observable<T> | undefined, deps: any[]): T | undefined
Usage Patterns
Pattern 1: Factory Function
This is the most common pattern. Use when the observable depends on props, state, or context:
import { use$ } from "@/hooks/use$";
import { useEventStore } from "@/hooks/useEventStore";
import { ProfileModel } from "applesauce-core/models";
function UserProfile({ pubkey }: { pubkey: string }) {
const store = useEventStore();
const profile = use$(
() => store.model(ProfileModel, pubkey),
[pubkey, store],
);
if (!profile) return <Skeleton />;
return <div>{profile.name}</div>;
}
Pattern 2: Direct Observable
Use for global observables that don't need to be recreated:
import { use$ } from "@/hooks/use$";
import { BehaviorSubject } from "rxjs";
const theme$ = new BehaviorSubject<"light" | "dark">("light");
function ThemeDisplay() {
const theme = use$(theme$);
return <div>Theme: {theme}</div>;
}
Pattern 3: Nested Cast Properties
Applesauce casts expose properties as observables:
import { use$ } from "@/hooks/use$";
import { Note } from "applesauce-common/casts";
function NoteCard({ note }: { note: Note }) {
const author = use$(note.author.profile$);
const reactions = use$(note.reactions$);
const replyCount = use$(note.replies?.count$);
return (
<div>
<span>{author?.name ?? "Anonymous"}</span>
<p>{note.content}</p>
<span>{reactions?.length ?? 0} reactions</span>
<span>{replyCount ?? 0} replies</span>
</div>
);
}
Pattern 5: Chained Observables
Combine multiple observables with RxJS operators:
import { use$ } from "@/hooks/use$";
import { combineLatest } from "rxjs";
import { map } from "rxjs/operators";
function ContactsWithRelays({ pubkey }: { pubkey: string }) {
const store = useEventStore();
const contacts = use$(() => {
const user = store.castUser(pubkey);
return user ? user.contacts$ : undefined;
}, [pubkey, store]);
const contactsWithOutboxes = use$(() => {
if (!contacts) return undefined;
return combineLatest(
contacts.map((contact) =>
contact.outboxes$.pipe(map((outboxes) => ({ contact, outboxes }))),
),
);
}, [contacts?.map((c) => c.pubkey).join(",")]);
return <div>...</div>;
}
Dependency Arrays: Critical Rules
The dependency array controls when the observable is recreated.
✅ DO: Include all variables used in factory
const profile = use$(
() => store.model(ProfileModel, pubkey),
[pubkey, store],
);
✅ DO: Serialize arrays and objects
const events = use$(
() => pool.subscription(relays, filters),
[relays.join(","), JSON.stringify(filters)],
);
const data = use$(
() => fetchData(contacts),
[contacts?.map((c) => c.pubkey).join(",")],
);
❌ DON'T: Pass array/object references directly
const events = use$(
() => pool.subscription(relays, filters),
[relays, filters],
);
❌ DON'T: Omit dependencies
const profile = use$(
() => store.model(ProfileModel, pubkey),
[],
);
Loading States
use$ returns undefined while waiting for the first value:
function UserProfile({ pubkey }: { pubkey: string }) {
const profile = use$(() => store.model(ProfileModel, pubkey), [pubkey]);
if (!profile) {
return <Skeleton />;
}
return <div>{profile.name}</div>;
}
Exception: BehaviorSubjects always have a current value:
const theme$ = new BehaviorSubject("light");
const theme = use$(theme$);
Common Mistakes to Avoid
1. Missing Dependencies
const profile = use$(() => store.model(ProfileModel, pubkey), []);
const profile = use$(() => store.model(ProfileModel, pubkey), [pubkey, store]);
2. Unstable Dependencies
const events = use$(() => store.timeline(filters), [filters]);
const events = use$(() => store.timeline(filters), [JSON.stringify(filters)]);
3. Conditional Hook Calls
if (condition) {
const data = use$(observable$);
}
const data = use$(() => (condition ? observable$ : undefined), [condition]);
4. Not Handling Undefined
const profile = use$(() => store.model(ProfileModel, pubkey), [pubkey]);
return <div>{profile.name}</div>;
const profile = use$(() => store.model(ProfileModel, pubkey), [pubkey]);
return <div>{profile?.name ?? "Loading..."}</div>;
Performance Tips
Avoid Creating New Arrays in Dependencies
const pubkeys = items.map((i) => i.pubkey);
const data = use$(() => fetch(pubkeys), [pubkeys]);
const data = use$(
() => fetch(items.map((i) => i.pubkey)),
[items.map((i) => i.pubkey).join(",")],
);
Memoize Complex Objects
const stableKey = useMemo(
() => JSON.stringify(complexConfig),
[complexConfig.field1, complexConfig.field2],
);
const data = use$(() => fetchData(complexConfig), [stableKey]);
Error Handling
Errors from observables are thrown and caught by React Error Boundaries:
import { ErrorBoundary } from "react-error-boundary";
function App() {
return (
<ErrorBoundary fallback={<ErrorFallback />}>
<ComponentWithObservable />
</ErrorBoundary>
);
}
Quick Reference
| Pattern | When to Use | Example |
|---|
| Factory function | Observable depends on props/state | use$(() => store.model(Model, id), [id]) |
| Direct observable | Global observable, no dependencies | use$(globalObservable$) |
| Nested properties | Cast properties like profile$, reactions$ | use$(note.author.profile$) |
| Side effects | Relay subscriptions, loaders | use$(() => pool.subscription(...), [deps]) |
| Chained observables | Combining multiple sources | use$(() => combineLatest([...]), [deps]) |
| Conditional | Optional observable | use$(() => cond ? obs$ : undefined, [cond]) |
Remember
- Always use the factory function pattern when observable depends on props/state
- Always include all used variables in the dependency array
- Always serialize arrays and objects in dependencies (
.join(), JSON.stringify())
- Always handle
undefined return values (except for BehaviorSubjects)
- Never call
use$ conditionally
- Never pass array/object references directly in dependencies