@snort/system-react
React hooks and components for building Nostr applications.
Installation
bun add @snort/system-reactOverview
@snort/system-react provides React hooks that integrate with @snort/system for reactive Nostr data fetching with built-in caching, SSR support, and automatic subscriptions.
Setup
Wrap your app with SnortContext.Provider:
import { SnortContext } from '@snort/system-react'
import { System } from './nostr'
function App() {
return (
<SnortContext.Provider value={System}>
<YourApp />
</SnortContext.Provider>
)
}Core Hooks
useUserProfile
Fetch and cache a user's profile. Uses useSyncExternalStore for efficient re-renders.
import { useUserProfile } from '@snort/system-react'
import type { CachedMetadata } from '@snort/system'
function UserProfile({ pubkey }: { pubkey: string }) {
const profile: CachedMetadata | undefined = useUserProfile(pubkey)
if (!profile) return <div>Loading...</div>
return (
<div>
<img src={profile.picture} alt={profile.display_name} />
<h1>{profile.display_name || profile.name}</h1>
<p>{profile.about}</p>
</div>
)
}With IntersectionObserver (auto-priority):
import { useUserProfile } from '@snort/system-react'
import { useRef } from 'react'
function ProfileCard({ pubkey }: { pubkey: string }) {
const ref = useRef<HTMLDivElement>(null)
// Automatically tracks visibility and adjusts cache priority
// Visible elements get "high" priority, off-screen get "normal"
const profile = useUserProfile(pubkey, ref)
return <div ref={ref}>{/* Profile content */}</div>
}Signature:
function useUserProfile(
pubKey?: string,
ref?: RefObject<Element | null>
): CachedMetadata | undefineduseRequestBuilder
Send a query to relays and get reactive results.
import { useRequestBuilder } from '@snort/system-react'
import { RequestBuilder, EventKind } from '@snort/system'
function Timeline() {
const rb = useMemo(() => {
const b = new RequestBuilder('timeline')
b.withFilter().kinds([EventKind.TextNote]).limit(50)
return b
}, [])
const events = useRequestBuilder(rb)
return (
<div>
{events.map(event => (
<div key={event.id}>
<p>{event.content}</p>
</div>
))}
</div>
)
}Signature:
function useRequestBuilder(rb: RequestBuilder): Array<TaggedNostrEvent>Internally this hook:
- Creates a query via
system.Query(rb)(memoized) - Subscribes to the
eventemitter viauseSyncExternalStore - Calls
q.uncancel()andq.start()on mount - Calls
q.cancel()andq.flush()on unmount
useRequestBuilderAdvanced
Same as useRequestBuilder but returns the full QueryLike object for manual control.
import { useRequestBuilderAdvanced } from '@snort/system-react'
import { RequestBuilder, EventKind } from '@snort/system'
function CustomFeed() {
const rb = useMemo(() => {
const b = new RequestBuilder('custom')
b.withFilter().kinds([EventKind.TextNote])
return b
}, [])
const query = useRequestBuilderAdvanced(rb)
return (
<div>
<p>Progress: {query.progress}</p>
{query.snapshot.map(event => (
<div key={event.id}>{event.content}</div>
))}
</div>
)
}useEventFeed
Fetch a single event by NostrLink.
import { useEventFeed } from '@snort/system-react'
import { NostrLink } from '@snort/system'
function EventView({ link }: { link: NostrLink }) {
const event = useEventFeed(link) // returns TaggedNostrEvent | undefined
if (!event) return <div>Loading...</div>
return <div>{event.content}</div>
}Signature:
function useEventFeed(link: NostrLink): TaggedNostrEvent | undefineduseEventsFeed
Fetch multiple events by an array of NostrLinks.
import { useEventsFeed } from '@snort/system-react'
import { NostrLink } from '@snort/system'
function ThreadView({ links }: { links: NostrLink[] }) {
const events = useEventsFeed('thread', links)
return (
<div>
{events.map(event => (
<div key={event.id}>{event.content}</div>
))}
</div>
)
}Signature:
function useEventsFeed(id: string, links: Array<NostrLink>): Array<TaggedNostrEvent>useReactions
Fetch reactions (likes, reposts, zaps) for a set of events.
import { useReactions } from '@snort/system-react'
function NoteWithReactions({ event }) {
const reactions = useReactions("reactions:" + event.id, [NostrLink.fromEvent(event)])
// ...
}Signature:
function useReactions(
subId: string,
ids: NostrLink | Array<NostrLink>,
others?: (rb: RequestBuilder) => void,
leaveOpen?: boolean,
): Array<TaggedNostrEvent>The others callback lets you add custom filters to the subscription. Must be wrapped in useCallback to prevent re-subscriptions.
useEventReactions
Parse reactions (likes, dislikes, reposts, zaps, etc.) from a pre-fetched set of related events.
import { useEventReactions } from '@snort/system-react'
function ReactionsBar({ event, relatedEvents }) {
const link = NostrLink.fromEvent(event)
const result = useEventReactions(link, relatedEvents)
// result.reactions.positive — positive reaction events
// result.reactions.negative — negative reaction events
// result.reactions.all — all reaction events
// result.reposts — repost events
// result.zaps — parsed zap receipts
// result.deletions — deletion events
// result.replies — text note replies
// result.others — other event kinds grouped by kind
}Signature:
function useEventReactions(
link: NostrLink,
related: ReadonlyArray<TaggedNostrEvent>,
assumeRelated?: boolean,
): {
deletions: TaggedNostrEvent[]
reactions: {
all: TaggedNostrEvent[]
positive: TaggedNostrEvent[]
negative: TaggedNostrEvent[]
}
replies: TaggedNostrEvent[]
reposts: TaggedNostrEvent[]
zaps: ParsedZap[]
others: Record<string, TaggedNostrEvent[]>
}This hook does not fetch data — it parses a pre-fetched array of related events. Use useReactions to fetch, then pass the results to useEventReactions for parsing.
useUserSearch
Search for users by name/npub.
import { useUserSearch } from '@snort/system-react'
function SearchComponent() {
const search = useUserSearch()
const results = await search('kieran')
// returns Array<string> of matching pubkeys
}useCached
Generic cached async-data hook backed by localStorage.
import { useCached } from '@snort/system-react'
function MyComponent() {
const { data, loading, error, reloadNow } = useCached<MyData>(
'cache-key', // undefined to disable caching
() => fetchMyData(), // async loader
120, // expire time in seconds (default: 120)
60, // cache error duration in seconds (optional)
)
}useSystemState
Get reactive system state snapshot.
import { useSystemState } from '@snort/system-react'
function SystemStatus() {
const state = useSystemState(System)
return (
<div>
<p>Active queries: {state.queries.length}</p>
</div>
)
}Signature:
function useSystemState(system: ExternalStore<SystemSnapshot>): SystemSnapshotContext
SnortContext
React context providing the SystemInterface instance.
import { SnortContext } from '@snort/system-react'
import { use } from 'react'
function MyComponent() {
const system = use(SnortContext)
// Use system directly
const query = system.Query(rb)
return <div>...</div>
}SSR Support
@snort/system-react has built-in SSR support:
import { hydrateSnort, getHydrationScript } from '@snort/system-react'
// Server-side
await System.Init()
const html = renderToString(<App />)
const script = getHydrationScript(System) // returns <script> tag with hydration data
// Client-side (call before React hydration)
hydrateSnort(System)Under the hood:
getHydrationScript()callsSystem.getHydrationData()and serializes it to a<script>tag that setswindow.__SNORT_HYDRATION__hydrateSnort()readswindow.__SNORT_HYDRATION__and callsSystem.hydrateQuery()for each entry, then cleans up the global
The useRequestBuilder hook eagerly creates queries during SSR, making data available after a FetchAll() pass.
Performance Features
Automatic Subscription Management
Hooks automatically subscribe/unsubscribe when components mount/unmount.
Cache Integration
All hooks use the system cache, avoiding duplicate fetches.
IntersectionObserver Support
useUserProfile with ref automatically adjusts cache priority based on visibility.
Per-Key Subscriptions
useUserProfile uses O(1) per-key subscription (profileLoader.cache.subscribe) instead of listening to the broad change event, reducing unnecessary re-renders.
Debug Components
TraceTimelineView / TraceTimelineOverlay / TraceStatsView
Debugging components for visualizing query trace timelines.
import { TraceTimelineView, TraceStatsView, TraceTimelineOverlay } from '@snort/system-react'See Also
- @snort/system - Core system
- @snort/shared - Utility functions
- @snort/wallet - Lightning payments
- Examples → React Hooks