@snort/system-react Examples
Real-world usage of React hooks from the Snort app.
useUserProfile with IntersectionObserver ref
The ref parameter enables viewport-aware priority loading — visible elements get "high" priority, off-screen get "normal". This is used on every profile image in timelines to avoid overloading relay requests for off-screen avatars:
// ProfileImage.tsx
const ref = useRef<HTMLDivElement>(null)
const user = useUserProfile(profile ? "" : pubkey, ref)
// ref is attached to the rendered wrapper divWhen a profile prop is already provided, the hook is given an empty string key (effectively a no-op). For NIP-05 verification, the hook can be conditionally disabled:
// Nip05.tsx
const spanRef = useRef<HTMLSpanElement>(null)
const profile = useUserProfile(pubkey && !nip05 ? pubkey : undefined, spanRef)Chained dependent queries with useRequestBuilder
Build a second query that depends on the first query's results:
// useAppHandler.ts
const sub = useMemo(() => {
if (!kind) return new RequestBuilder("empty")
const sub = new RequestBuilder(`app-handler:${kind}`)
sub.withFilter().kinds([31990 as EventKind]).tag("k", [kind.toString()])
return sub
}, [kind])
const dataApps = useRequestBuilder(sub)
// Second query uses results from first
const recommendsSub = useMemo(() => {
if (!kind || dataApps.length === 0) return new RequestBuilder("empty-recommends")
const rb = new RequestBuilder(`app-handler:${kind}:recommends`)
rb.withFilter()
.kinds([31989 as EventKind])
.replyToLink(dataApps.map(a => NostrLink.fromEvent(a)))
return rb
}, [kind, dataApps.length])
const dataRecommends = useRequestBuilder(recommendsSub)Timeline with "show new posts" using useRequestBuilderAdvanced
The only usage of useRequestBuilderAdvanced in the app — enables a realtime subscription that collects new events, merged into the main feed on user action:
// TimelineFeed.ts
const mainQuery = useRequestBuilderAdvanced(sub)
const main = useSyncExternalStore(
h => {
mainQuery.uncancel()
mainQuery.on("event", h)
mainQuery.start()
return () => {
mainQuery.flush()
mainQuery.cancel()
mainQuery.off("event", h)
}
},
() => mainQuery?.snapshot,
)
// Realtime "latest" query
const subRealtime = useMemo(() => {
const rb = createBuilder()
rb.id = `${rb.id}:latest`
rb.withOptions({ leaveOpen: true })
for (const filter of rb.filterBuilders) {
filter.limit(1).since(now)
}
return rb
}, [createBuilder, now])
const latestQuery = useRequestBuilderAdvanced(subRealtime)
const latest = useSyncExternalStore(/* same pattern */)
// Merge new events on user action
showLatest: () => {
if (latest) {
mainQuery?.feed.add(latest)
latestQuery?.feed.clear()
}
}Chaining useEventFeed → useEventsFeed
Load a profile badges event, extract its tags as NostrLink[], then fetch all linked badge events:
// BadgesFeed.ts
const profileBadgesLink = new NostrLink(NostrPrefix.Address, "profile_badges", EventKind.ProfileBadges, pubkey)
const profileBadges = useEventFeed(profileBadgesLink)
const links = NostrLink.fromTags(profileBadges?.tags ?? [])
const linkedEvents = useEventsFeed(`badges:${pubkey}`, links)Reactions pipeline: useReactions → useEventReactions
Fetch all reaction/repost/zap events for a note, filter out muted pubkeys, then parse into categorized groups:
// NoteContext.tsx
const link = useMemo(() => NostrLink.fromEvent(ev), [ev.id])
const relatedRaw = useReactions(`reactions:${link.tagKey}`, link)
const related = useMemo(() => relatedRaw.filter(a => !isMuted(a.pubkey)), [relatedRaw, isMuted])
const reactions = useEventReactions(link, related)
// reactions.reactions.positive, .negative, .all
// reactions.reposts, reactions.zaps, reactions.replies, reactions.deletionsuseCached for API data, NIP-05 verification, and relay-backed cache
API data caching with custom expiry:
// Referrals.tsx
const loader = useCallback(() => {
const api = new SnortApi(undefined, publisher?.signer)
return api.getRefCode()
}, [publisher])
const { data: refCode, reloadNow } = useCached<RefCodeResponse>(
publisher ? `ref:${publisher.pubKey}` : undefined,
loader,
60 * 60 * 24, // 24 hours
)NIP-05 verification with viewport gating:
// Nip05.tsx
const { data, error } = useCached(
toSplit && inView && pubkey ? `nip5:${toSplit}` : undefined,
async () => await fetchNip05PubkeyWithThrow(name, domain),
Day, // 1 day TTL
)Key is undefined when not in view or no pubkey, preventing unnecessary fetches.
Relay-backed cache using system.Fetch as loader:
// ArticlesFeed.ts
const loader = useCallback(async () => {
return await system.Fetch(sub)
}, [sub, system])
const { data } = useCached("articles", loader, Hour * 6)SnortContext consumption via React 19 use()
The app uses React 19's use() API instead of useContext():
// useEventPublisher.tsx
const system = use(SnortContext)
// Direct cache access (synchronous, no subscription)
// Bookmarks.tsx
const profile = system.config.profiles.getFromCache(p)
// Connection pool access
// RelayState.ts
const c = system.pool.getConnection(addr)useUserSearch with debouncing
// NewChatWindow.tsx
const search = useUserSearch()
useEffect(() => {
return debounce(500, () => {
if (term) {
search(term).then(setResults)
} else {
setResults(followList)
}
})
}, [term, followList, search])