一键导入
react-rxjs-observables
Use RxJS observables in React components with the use$ hook from applesauce-react
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use RxJS observables in React components with the use$ hook from applesauce-react
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| 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"} |
This skill teaches you how to integrate RxJS observables into React components using the use$ hook from applesauce-react.
Use this skill when you need to:
note.author.profile$, user.contacts$, etc.combineLatest, switchMap, or other RxJS operatorsuse$Import from your hooks directory:
import { use$ } from "@/hooks/use$";
// Direct BehaviorSubject - always returns a value
use$<T>(observable?: BehaviorSubject<T>): T
// Direct Observable - may return undefined if no value emitted yet
use$<T>(observable?: Observable<T>): T | undefined
// Factory function with dependencies - MOST COMMON
use$<T>(factory: () => Observable<T> | undefined, deps: any[]): T | undefined
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();
// Factory recreates observable when pubkey or store changes
const profile = use$(
() => store.model(ProfileModel, pubkey),
[pubkey, store],
);
if (!profile) return <Skeleton />;
return <div>{profile.name}</div>;
}
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>;
}
Applesauce casts expose properties as observables:
import { use$ } from "@/hooks/use$";
import { Note } from "applesauce-common/casts";
function NoteCard({ note }: { note: Note }) {
// Subscribe to nested observables
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>
);
}
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]);
// Combine each contact's outboxes
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>;
}
The dependency array controls when the observable is recreated.
const profile = use$(
() => store.model(ProfileModel, pubkey),
[pubkey, store], // Both used in factory
);
// For arrays - use .join()
const events = use$(
() => pool.subscription(relays, filters),
[relays.join(","), JSON.stringify(filters)],
);
// For optional arrays - use optional chaining
const data = use$(
() => fetchData(contacts),
[contacts?.map((c) => c.pubkey).join(",")],
);
// WRONG - infinite re-subscriptions!
const events = use$(
() => pool.subscription(relays, filters),
[relays, filters], // References change every render
);
// WRONG - stale data!
const profile = use$(
() => store.model(ProfileModel, pubkey),
[], // pubkey changes won't update!
);
use$ returns undefined while waiting for the first value:
function UserProfile({ pubkey }: { pubkey: string }) {
const profile = use$(() => store.model(ProfileModel, pubkey), [pubkey]);
// Always handle undefined
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$); // Never undefined
// ❌ WRONG
const profile = use$(() => store.model(ProfileModel, pubkey), []);
// ✅ CORRECT
const profile = use$(() => store.model(ProfileModel, pubkey), [pubkey, store]);
// ❌ WRONG
const events = use$(() => store.timeline(filters), [filters]);
// ✅ CORRECT
const events = use$(() => store.timeline(filters), [JSON.stringify(filters)]);
// ❌ WRONG - breaks rules of hooks
if (condition) {
const data = use$(observable$);
}
// ✅ CORRECT
const data = use$(() => (condition ? observable$ : undefined), [condition]);
// ❌ WRONG - runtime error
const profile = use$(() => store.model(ProfileModel, pubkey), [pubkey]);
return <div>{profile.name}</div>; // Error if undefined!
// ✅ CORRECT
const profile = use$(() => store.model(ProfileModel, pubkey), [pubkey]);
return <div>{profile?.name ?? "Loading..."}</div>;
// ❌ Bad - creates new array every render
const pubkeys = items.map((i) => i.pubkey);
const data = use$(() => fetch(pubkeys), [pubkeys]);
// ✅ Good - stable string reference
const data = use$(
() => fetch(items.map((i) => i.pubkey)),
[items.map((i) => i.pubkey).join(",")],
);
const stableKey = useMemo(
() => JSON.stringify(complexConfig),
[complexConfig.field1, complexConfig.field2],
);
const data = use$(() => fetchData(complexConfig), [stableKey]);
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>
);
}
| 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]) |
.join(), JSON.stringify())undefined return values (except for BehaviorSubjects)use$ conditionallyUse `resilientSubscription` / `resilientRequest` from `@/lib/resilientSubscription` to fetch Nostr events from relays. Activates when querying relays, building a custom observable pipeline, paginating a feed, computing reactive counts from the EventStore, or working with tag filters that the base `Filter` type doesn't include.
Build comment threads on non-kind-1 Nostr events using NIP-22 (kind:1111). Activates when adding replies/comments to NIP-34 issues/patches, NIP-23 articles, or any non-kind-1 root event, or when querying comments by thread root.
Add fonts via @fontsource, change color schemes, manage light/dark themes via CSS custom properties, and avoid the `isolate` + negative-z-index gotcha that hides background images. Activates when adjusting brand colors, adding fonts, switching themes, or building decorative backgrounds.