use$ Hook
The use$ hook is a powerful utility that combines useObservableState and useMemo to make working with RxJS observables in React components seamless. It automatically subscribes to observables and updates your component when values change, making it perfect for integrating with the EventStore, RelayPool, and other reactive data sources.
Overview
The use$ hook provides a simple way to subscribe to observables in React components. It handles subscription management, cleanup, and state updates automatically, so you can focus on building your UI.
API
// Direct observable (returns T for BehaviorSubject, T | undefined for Observable)
use$<T>(observable?: BehaviorSubject<T>): T;
use$<T>(observable?: Observable<T>): T | undefined;
// Factory function with dependencies (returns T | undefined)
use$<T>(factory: () => Observable<T> | undefined, deps: any[]): T | undefined;Usage Patterns
1. Direct Observable Subscription
The simplest usage is to pass an observable directly:
import { use$ } from "applesauce-react/hooks";
import { BehaviorSubject } from "rxjs";
const user$ = new BehaviorSubject<User | null>(null);
function MyComponent() {
const user = use$(user$);
if (!user) return <div>Not logged in</div>;
return <div>Welcome, {user.name}!</div>;
}2. Factory Function with Dependencies
When you need to create observables based on props or other reactive values, use the factory pattern:
function Profile({ pubkey }: { pubkey: string }) {
const profile = use$(() => eventStore.profile(pubkey), [pubkey]);
return <div>{profile?.displayName || "Loading..."}</div>;
}3. Conditional Observables
The factory pattern is especially useful when you need conditional observables:
function Mailboxes({ pubkey }: { pubkey: string | null }) {
const mailboxes = use$(() => (pubkey ? eventStore.mailboxes(pubkey) : undefined), [pubkey]);
return <div>{mailboxes?.outboxes.length || 0} outboxes</div>;
}Common Use Cases
Working with EventStore
The EventStore provides many observable methods that work perfectly with use$:
Profiles
function UserProfile({ user }: { user: User }) {
const profile = use$(user.profile$);
return (
<div>
<img src={profile?.picture} alt={profile?.displayName} />
<h2>{profile?.displayName || user.npub}</h2>
</div>
);
}Models
function Comments({ article }: { article: Article }) {
const comments = use$(() => eventStore.model(CommentsModel, article.event), [article.id]);
return (
<div>
{comments?.map((comment) => (
<CommentItem key={comment.id} comment={comment} />
))}
</div>
);
}Event Lookups
function EventView({ pointer }: { pointer: EventPointer | null }) {
const event = use$(() => (pointer ? eventStore.event(pointer.id) : undefined), [pointer?.id]);
if (!event) return <div>Loading...</div>;
return <div>{event.content}</div>;
}Timelines
function Timeline({ filters }: { filters: Filter }) {
const events = use$(() => eventStore.timeline(filters), [JSON.stringify(filters)]);
return (
<div>
{events?.map((event) => (
<EventCard key={event.id} event={event} />
))}
</div>
);
}Working with RelayPool
The RelayPool provides observables for relay information and subscriptions:
function RelayInfo({ relay }: { relay: string }) {
const info = use$(() => pool.relay(relay).information$, [relay]);
return (
<div>
<h3>{info?.name || relay}</h3>
<img src={info?.icon} alt={relay} />
</div>
);
}Working with BehaviorSubjects
use$ works seamlessly with BehaviorSubjects, which always have a current value:
const signer$ = new BehaviorSubject<Signer | null>(null);
const pubkey$ = new BehaviorSubject<string | null>(null);
function App() {
const signer = use$(signer$);
const pubkey = use$(pubkey$);
if (!signer || !pubkey) {
return <LoginView />;
}
return <MainApp />;
}Complex Observable Chains
You can use use$ with complex RxJS operator chains:
function ArticleList({ relay }: { relay: string }) {
const articles = use$(
() =>
pool
.relay(relay)
.subscription({ kinds: [30023] })
.pipe(
onlyEvents(),
mapEventsToStore(eventStore),
mapEventsToTimeline(),
castTimelineStream(Article, eventStore),
),
[relay],
);
return (
<div>
{articles?.map((article) => (
<ArticleCard key={article.id} article={article} />
))}
</div>
);
}Side Effects with use$
You can use use$ for side effects by creating observables that don't return values:
function ArticleViewer({ article }: { article: Article }) {
// Subscribe to comments for side effects (loading them)
use$(() => {
if (!article) return;
return pool
.relay(relay)
.subscription({
kinds: [1111],
"#a": [`30023:${article.pubkey}:${article.id}`],
})
.pipe(onlyEvents(), mapEventsToStore(eventStore));
}, [article?.id, relay]);
// Then use the model to display comments
const comments = use$(() => eventStore.model(CommentsModel, article.event), [article.id]);
return <CommentsList comments={comments} />;
}Best Practices
1. Use Factory Pattern for Dynamic Observables
When observables depend on props or state, always use the factory pattern with dependencies:
// ✅ Good - factory with dependencies
const profile = use$(() => eventStore.profile(pubkey), [pubkey]);
// ❌ Bad - creates new observable on every render
const profile = use$(eventStore.profile(pubkey));2. Handle Undefined States
Remember that use$ returns undefined until the observable emits a value:
function Profile({ pubkey }: { pubkey: string }) {
const profile = use$(() => eventStore.profile(pubkey), [pubkey]);
// Handle loading state
if (profile === undefined) {
return <div>Loading profile...</div>;
}
// Handle missing profile
if (!profile) {
return <div>Profile not found</div>;
}
return <div>{profile.displayName}</div>;
}3. Use BehaviorSubject for Always-Available Values
For values that should always be available (like current user), use BehaviorSubject:
// BehaviorSubject always has a value
const user$ = new BehaviorSubject<User | null>(null);
const user = use$(user$); // user is User | null, never undefined
// Regular Observable might not have emitted yet
const user$ = new Subject<User>();
const user = use$(user$); // user is User | undefined4. Memoize Complex Dependencies
For complex dependency arrays, consider using useMemo or stringifying objects:
// ✅ Good - stringify complex objects
const events = use$(() => eventStore.timeline(filters), [JSON.stringify(filters)]);
// ✅ Also good - useMemo for complex dependencies
const filterKey = useMemo(() => JSON.stringify(filters), [filters]);
const events = use$(() => eventStore.timeline(filters), [filterKey]);5. Chain Observable Properties
Many objects in applesauce expose observables as properties. You can chain them:
function ContactCard({ contact }: { contact: User }) {
const profile = use$(contact.profile$);
const nutzapInfo = use$(contact.nutzap$);
const contacts = use$(contact.contacts$);
return (
<div>
<h3>{profile?.displayName}</h3>
{nutzapInfo && <div>Can receive zaps</div>}
<div>{contacts?.length || 0} contacts</div>
</div>
);
}How It Works
The use$ hook:
- Memoizes the observable using
useMemoto prevent unnecessary re-subscriptions - Subscribes synchronously during the initial render to get immediate values when available
- Updates React state when the observable emits new values
- Cleans up subscriptions automatically when the component unmounts or dependencies change
- Handles errors by throwing them to React error boundaries
This makes it safe to use with both hot and cold observables, and ensures your components always reflect the latest values from your reactive data sources.
Type Safety
use$ provides full TypeScript support:
- For
BehaviorSubject<T>, it returnsT(never undefined) - For
Observable<T>, it returnsT | undefined - The factory pattern preserves types from your observable
// TypeScript knows profile is Profile | undefined
const profile = use$(() => eventStore.profile(pubkey), [pubkey]);
// TypeScript knows user is User | null (from BehaviorSubject)
const user = use$(user$);