From 27f92336ae1fecb2e2d59b8be1cf503793f34ca3 Mon Sep 17 00:00:00 2001 From: mleku Date: Thu, 6 Nov 2025 14:34:06 +0000 Subject: [PATCH] Add NDK skill documentation and examples - Introduced comprehensive documentation for the Nostr Development Kit (NDK) including an overview, quick reference, and troubleshooting guide. - Added detailed examples covering initialization, authentication, event publishing, querying, and user profile management. - Structured the documentation to facilitate quick lookups and deep learning, based on real-world usage patterns from the Plebeian Market application. - Created an index for examples to enhance usability and navigation. - Bumped version to 1.0.0 to reflect the addition of this new skill set. --- .claude/skills/ndk/INDEX.md | 286 +++++ .claude/skills/ndk/README.md | 38 + .../skills/ndk/examples/01-initialization.ts | 162 +++ .../skills/ndk/examples/02-authentication.ts | 255 ++++ .../ndk/examples/03-publishing-events.ts | 376 ++++++ .../ndk/examples/04-querying-subscribing.ts | 404 +++++++ .../skills/ndk/examples/05-users-profiles.ts | 423 +++++++ .claude/skills/ndk/examples/README.md | 94 ++ .claude/skills/ndk/ndk-skill.md | 701 +++++++++++ .claude/skills/ndk/quick-reference.md | 351 ++++++ .claude/skills/ndk/troubleshooting.md | 530 +++++++++ .claude/skills/react/README.md | 119 ++ .claude/skills/react/SKILL.md | 1026 +++++++++++++++++ .../react/examples/practical-patterns.tsx | 878 ++++++++++++++ .../react/references/hooks-quick-reference.md | 291 +++++ .../skills/react/references/performance.md | 658 +++++++++++ .../react/references/server-components.md | 656 +++++++++++ .claude/skills/typescript/README.md | 133 +++ .claude/skills/typescript/SKILL.md | 359 ++++++ .claude/skills/typescript/examples/README.md | 45 + .../typescript/examples/advanced-types.ts | 478 ++++++++ .../typescript/examples/react-patterns.ts | 555 +++++++++ .../typescript/examples/type-system-basics.ts | 361 ++++++ .claude/skills/typescript/quick-reference.md | 395 +++++++ .../typescript/references/common-patterns.md | 756 ++++++++++++ .../typescript/references/type-system.md | 804 +++++++++++++ .../typescript/references/utility-types.md | 666 +++++++++++ 27 files changed, 11800 insertions(+) create mode 100644 .claude/skills/ndk/INDEX.md create mode 100644 .claude/skills/ndk/README.md create mode 100644 .claude/skills/ndk/examples/01-initialization.ts create mode 100644 .claude/skills/ndk/examples/02-authentication.ts create mode 100644 .claude/skills/ndk/examples/03-publishing-events.ts create mode 100644 .claude/skills/ndk/examples/04-querying-subscribing.ts create mode 100644 .claude/skills/ndk/examples/05-users-profiles.ts create mode 100644 .claude/skills/ndk/examples/README.md create mode 100644 .claude/skills/ndk/ndk-skill.md create mode 100644 .claude/skills/ndk/quick-reference.md create mode 100644 .claude/skills/ndk/troubleshooting.md create mode 100644 .claude/skills/react/README.md create mode 100644 .claude/skills/react/SKILL.md create mode 100644 .claude/skills/react/examples/practical-patterns.tsx create mode 100644 .claude/skills/react/references/hooks-quick-reference.md create mode 100644 .claude/skills/react/references/performance.md create mode 100644 .claude/skills/react/references/server-components.md create mode 100644 .claude/skills/typescript/README.md create mode 100644 .claude/skills/typescript/SKILL.md create mode 100644 .claude/skills/typescript/examples/README.md create mode 100644 .claude/skills/typescript/examples/advanced-types.ts create mode 100644 .claude/skills/typescript/examples/react-patterns.ts create mode 100644 .claude/skills/typescript/examples/type-system-basics.ts create mode 100644 .claude/skills/typescript/quick-reference.md create mode 100644 .claude/skills/typescript/references/common-patterns.md create mode 100644 .claude/skills/typescript/references/type-system.md create mode 100644 .claude/skills/typescript/references/utility-types.md diff --git a/.claude/skills/ndk/INDEX.md b/.claude/skills/ndk/INDEX.md new file mode 100644 index 0000000..41d6d98 --- /dev/null +++ b/.claude/skills/ndk/INDEX.md @@ -0,0 +1,286 @@ +# NDK (Nostr Development Kit) Claude Skill + +> **Comprehensive knowledge base for working with NDK in production applications** + +This Claude skill provides deep expertise in the Nostr Development Kit based on real-world usage patterns from the Plebeian Market application. + +## ๐Ÿ“š Documentation Structure + +``` +.claude/skills/ndk/ +โ”œโ”€โ”€ README.md # This file - Overview and getting started +โ”œโ”€โ”€ ndk-skill.md # Complete reference guide (18KB) +โ”œโ”€โ”€ quick-reference.md # Fast lookup for common tasks (7KB) +โ”œโ”€โ”€ troubleshooting.md # Common problems and solutions +โ””โ”€โ”€ examples/ # Production code examples + โ”œโ”€โ”€ README.md + โ”œโ”€โ”€ 01-initialization.ts # NDK setup and connection + โ”œโ”€โ”€ 02-authentication.ts # NIP-07, NIP-46, private keys + โ”œโ”€โ”€ 03-publishing-events.ts # Creating and publishing events + โ”œโ”€โ”€ 04-querying-subscribing.ts # Fetching and real-time subs + โ””โ”€โ”€ 05-users-profiles.ts # User and profile management +``` + +## ๐Ÿš€ Quick Start + +### For Quick Lookups +Start with **`quick-reference.md`** for: +- Common code snippets +- Quick syntax reminders +- Frequently used patterns + +### For Deep Learning +Read **`ndk-skill.md`** for: +- Complete API documentation +- Best practices +- Integration patterns +- Performance optimization + +### For Problem Solving +Check **`troubleshooting.md`** for: +- Common error solutions +- Performance tips +- Testing strategies +- Debug techniques + +### For Code Examples +Browse **`examples/`** directory for: +- Real production code +- Full implementations +- React integration patterns +- Error handling examples + +## ๐Ÿ“– Core Topics Covered + +### 1. Initialization & Setup +- Basic NDK initialization +- Multiple instance patterns (main + zap relays) +- Connection management with timeouts +- Relay pool configuration +- Connection status monitoring + +### 2. Authentication +- **NIP-07**: Browser extension signers (Alby, nos2x) +- **NIP-46**: Remote signers (Bunker) +- **Private Keys**: Direct key management +- Auto-login with localStorage +- Multi-account session management + +### 3. Event Publishing +- Basic text notes +- Parameterized replaceable events (products, profiles) +- Order and payment events +- Batch publishing +- Error handling patterns + +### 4. Querying & Subscriptions +- One-time fetches with `fetchEvents()` +- Real-time subscriptions +- Tag filtering patterns +- Time-range queries +- Event monitoring +- React Query integration + +### 5. User & Profile Management +- Fetch profiles (npub, hex, NIP-05) +- Update user profiles +- Follow/unfollow operations +- Batch profile loading +- Profile caching strategies + +### 6. Advanced Patterns +- Store-based NDK management +- Query + subscription combination +- Event parsing utilities +- Memory leak prevention +- Performance optimization + +## ๐ŸŽฏ Use Cases + +### Building a Nostr Client +```typescript +// Initialize +const { ndk, isConnected } = await initializeNDK({ + relays: ['wss://relay.damus.io', 'wss://nos.lol'], + timeoutMs: 10000 +}) + +// Authenticate +const { user } = await loginWithExtension(ndk) + +// Publish +await publishBasicNote(ndk, 'Hello Nostr!') + +// Subscribe +const sub = subscribeToNotes(ndk, user.pubkey, (event) => { + console.log('New note:', event.content) +}) +``` + +### Building a Marketplace +```typescript +// Publish product +await publishProduct(ndk, { + slug: 'bitcoin-shirt', + title: 'Bitcoin T-Shirt', + price: 25, + currency: 'USD', + images: ['https://...'] +}) + +// Create order +await createOrder(ndk, { + orderId: uuidv4(), + sellerPubkey: merchant.pubkey, + productRef: '30402:pubkey:bitcoin-shirt', + quantity: 1, + totalAmount: '25.00' +}) + +// Monitor payment +monitorPaymentReceipt(ndk, orderId, invoiceId, (preimage) => { + console.log('Payment confirmed!') +}) +``` + +### React Integration +```typescript +function Feed() { + const ndk = useNDK() + const { user } = useAuth() + + // Query with real-time updates + const { data: notes } = useNotesWithSubscription( + ndk, + user.pubkey + ) + + return ( +
+ {notes?.map(note => ( + + ))} +
+ ) +} +``` + +## ๐Ÿ” Common Patterns Quick Reference + +### Safe NDK Access +```typescript +const ndk = ndkActions.getNDK() +if (!ndk) throw new Error('NDK not initialized') +``` + +### Subscription Cleanup +```typescript +useEffect(() => { + const sub = ndk.subscribe(filter, { closeOnEose: false }) + sub.on('event', handleEvent) + return () => sub.stop() // Critical! +}, [ndk]) +``` + +### Error Handling +```typescript +try { + await event.sign() + await event.publish() +} catch (error) { + console.error('Publishing failed:', error) + throw new Error('Failed to publish. Check connection.') +} +``` + +### Tag Filtering +```typescript +// โœ… Correct (note the # prefix for tag filters) +{ kinds: [16], '#order': [orderId] } + +// โŒ Wrong +{ kinds: [16], 'order': [orderId] } +``` + +## ๐Ÿ›  Development Tools + +### VS Code Integration +These skill files work with: +- Cursor AI for code completion +- Claude for code assistance +- GitHub Copilot with context + +### Debugging Tips +```typescript +// Check connection +console.log('Connected relays:', + Array.from(ndk.pool?.relays.values() || []) + .filter(r => r.status === 1) + .map(r => r.url) +) + +// Verify signer +console.log('Signer:', ndk.signer) +console.log('Active user:', ndk.activeUser) + +// Event inspection +console.log('Event:', { + id: event.id, + kind: event.kind, + tags: event.tags, + sig: event.sig +}) +``` + +## ๐Ÿ“Š Statistics + +- **Total Documentation**: ~50KB +- **Code Examples**: 5 complete modules +- **Patterns Documented**: 50+ +- **Common Issues Covered**: 15+ +- **Based On**: Real production code + +## ๐Ÿ”— Additional Resources + +### Official NDK Resources +- **GitHub**: https://github.com/nostr-dev-kit/ndk +- **Documentation**: https://ndk.fyi +- **NPM**: `@nostr-dev-kit/ndk` + +### Nostr Protocol +- **NIPs**: https://github.com/nostr-protocol/nips +- **Nostr**: https://nostr.com + +### Related Tools +- **TanStack Query**: React state management +- **TanStack Router**: Type-safe routing +- **Radix UI**: Accessible components + +## ๐Ÿ’ก Tips for Using This Skill + +1. **Start Small**: Begin with quick-reference.md for syntax +2. **Go Deep**: Read ndk-skill.md section by section +3. **Copy Examples**: Use examples/ as templates +4. **Debug Issues**: Check troubleshooting.md first +5. **Stay Updated**: Patterns based on production usage + +## ๐Ÿค Contributing + +This skill is maintained based on the Plebeian Market codebase. To improve it: + +1. Document new patterns you discover +2. Add solutions to common problems +3. Update examples with better approaches +4. Keep synchronized with NDK updates + +## ๐Ÿ“ Version Info + +- **Skill Version**: 1.0.0 +- **NDK Version**: Latest (based on production usage) +- **Last Updated**: November 2025 +- **Codebase**: Plebeian Market + +--- + +**Ready to build with NDK?** Start with `quick-reference.md` or dive into `examples/01-initialization.ts`! + diff --git a/.claude/skills/ndk/README.md b/.claude/skills/ndk/README.md new file mode 100644 index 0000000..2a13dc4 --- /dev/null +++ b/.claude/skills/ndk/README.md @@ -0,0 +1,38 @@ +# NDK (Nostr Development Kit) Claude Skill + +This skill provides comprehensive knowledge about working with the Nostr Development Kit (NDK) library. + +## Files + +- **ndk-skill.md** - Complete reference documentation with patterns from production usage +- **quick-reference.md** - Quick lookup guide for common NDK tasks +- **examples/** - Code examples extracted from the Plebeian Market codebase + +## Usage + +When working with NDK-related code, reference these documents to: +- Understand initialization patterns +- Learn authentication flows (NIP-07, NIP-46, private keys) +- Implement event creation and publishing +- Set up subscriptions for real-time updates +- Query events with filters +- Handle users and profiles +- Integrate with TanStack Query + +## Key Topics Covered + +1. NDK Initialization & Configuration +2. Authentication & Signers +3. Event Creation & Publishing +4. Querying Events +5. Real-time Subscriptions +6. User & Profile Management +7. Tag Handling +8. Replaceable Events +9. Relay Management +10. Integration with React/TanStack Query +11. Error Handling & Best Practices +12. Performance Optimization + +All examples are based on real production code from the Plebeian Market application. + diff --git a/.claude/skills/ndk/examples/01-initialization.ts b/.claude/skills/ndk/examples/01-initialization.ts new file mode 100644 index 0000000..bada6d6 --- /dev/null +++ b/.claude/skills/ndk/examples/01-initialization.ts @@ -0,0 +1,162 @@ +/** + * NDK Initialization Patterns + * + * Examples from: src/lib/stores/ndk.ts + */ + +import NDK from '@nostr-dev-kit/ndk' + +// ============================================================ +// BASIC INITIALIZATION +// ============================================================ + +const basicInit = () => { + const ndk = new NDK({ + explicitRelayUrls: ['wss://relay.damus.io', 'wss://relay.nostr.band'] + }) + + return ndk +} + +// ============================================================ +// PRODUCTION PATTERN - WITH MULTIPLE NDK INSTANCES +// ============================================================ + +const productionInit = (relays: string[], zapRelays: string[]) => { + // Main NDK instance for general operations + const ndk = new NDK({ + explicitRelayUrls: relays + }) + + // Separate NDK for zap operations (performance optimization) + const zapNdk = new NDK({ + explicitRelayUrls: zapRelays + }) + + return { ndk, zapNdk } +} + +// ============================================================ +// CONNECTION WITH TIMEOUT +// ============================================================ + +const connectWithTimeout = async ( + ndk: NDK, + timeoutMs: number = 10000 +): Promise => { + // Create connection promise + const connectPromise = ndk.connect() + + // Create timeout promise + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Connection timeout')), timeoutMs) + ) + + try { + // Race between connection and timeout + await Promise.race([connectPromise, timeoutPromise]) + console.log('โœ… NDK connected successfully') + } catch (error) { + if (error instanceof Error && error.message === 'Connection timeout') { + console.error('โŒ Connection timed out after', timeoutMs, 'ms') + } else { + console.error('โŒ Connection failed:', error) + } + throw error + } +} + +// ============================================================ +// FULL INITIALIZATION FLOW +// ============================================================ + +interface InitConfig { + relays?: string[] + zapRelays?: string[] + timeoutMs?: number +} + +const defaultRelays = [ + 'wss://relay.damus.io', + 'wss://relay.nostr.band', + 'wss://nos.lol' +] + +const defaultZapRelays = [ + 'wss://relay.damus.io', + 'wss://nostr.wine' +] + +const initializeNDK = async (config: InitConfig = {}) => { + const { + relays = defaultRelays, + zapRelays = defaultZapRelays, + timeoutMs = 10000 + } = config + + // Initialize instances + const ndk = new NDK({ explicitRelayUrls: relays }) + const zapNdk = new NDK({ explicitRelayUrls: zapRelays }) + + // Connect with timeout protection + try { + await connectWithTimeout(ndk, timeoutMs) + await connectWithTimeout(zapNdk, timeoutMs) + + return { ndk, zapNdk, isConnected: true } + } catch (error) { + return { ndk, zapNdk, isConnected: false, error } + } +} + +// ============================================================ +// CHECKING CONNECTION STATUS +// ============================================================ + +const getConnectionStatus = (ndk: NDK) => { + const connectedRelays = Array.from(ndk.pool?.relays.values() || []) + .filter(relay => relay.status === 1) + .map(relay => relay.url) + + const isConnected = connectedRelays.length > 0 + + return { + isConnected, + connectedRelays, + totalRelays: ndk.pool?.relays.size || 0 + } +} + +// ============================================================ +// USAGE EXAMPLE +// ============================================================ + +async function main() { + // Initialize + const { ndk, zapNdk, isConnected } = await initializeNDK({ + relays: defaultRelays, + zapRelays: defaultZapRelays, + timeoutMs: 10000 + }) + + if (!isConnected) { + console.error('Failed to connect to relays') + return + } + + // Check status + const status = getConnectionStatus(ndk) + console.log('Connection status:', status) + + // Ready to use + console.log('NDK ready for operations') +} + +export { + basicInit, + productionInit, + connectWithTimeout, + initializeNDK, + getConnectionStatus +} + diff --git a/.claude/skills/ndk/examples/02-authentication.ts b/.claude/skills/ndk/examples/02-authentication.ts new file mode 100644 index 0000000..2356205 --- /dev/null +++ b/.claude/skills/ndk/examples/02-authentication.ts @@ -0,0 +1,255 @@ +/** + * NDK Authentication Patterns + * + * Examples from: src/lib/stores/auth.ts + */ + +import NDK from '@nostr-dev-kit/ndk' +import { NDKNip07Signer, NDKPrivateKeySigner, NDKNip46Signer } from '@nostr-dev-kit/ndk' + +// ============================================================ +// NIP-07 - BROWSER EXTENSION SIGNER +// ============================================================ + +const loginWithExtension = async (ndk: NDK) => { + try { + // Create NIP-07 signer (browser extension like Alby, nos2x) + const signer = new NDKNip07Signer() + + // Wait for signer to be ready + await signer.blockUntilReady() + + // Set signer on NDK instance + ndk.signer = signer + + // Get authenticated user + const user = await signer.user() + + console.log('โœ… Logged in via extension:', user.npub) + return { user, signer } + } catch (error) { + console.error('โŒ Extension login failed:', error) + throw new Error('Failed to login with browser extension. Is it installed?') + } +} + +// ============================================================ +// PRIVATE KEY SIGNER +// ============================================================ + +const loginWithPrivateKey = async (ndk: NDK, privateKeyHex: string) => { + try { + // Validate private key format (64 hex characters) + if (!/^[0-9a-f]{64}$/.test(privateKeyHex)) { + throw new Error('Invalid private key format') + } + + // Create private key signer + const signer = new NDKPrivateKeySigner(privateKeyHex) + + // Wait for signer to be ready + await signer.blockUntilReady() + + // Set signer on NDK instance + ndk.signer = signer + + // Get authenticated user + const user = await signer.user() + + console.log('โœ… Logged in with private key:', user.npub) + return { user, signer } + } catch (error) { + console.error('โŒ Private key login failed:', error) + throw error + } +} + +// ============================================================ +// NIP-46 - REMOTE SIGNER (BUNKER) +// ============================================================ + +const loginWithNip46 = async ( + ndk: NDK, + bunkerUrl: string, + localPrivateKey?: string +) => { + try { + // Create or use existing local signer + const localSigner = localPrivateKey + ? new NDKPrivateKeySigner(localPrivateKey) + : NDKPrivateKeySigner.generate() + + // Create NIP-46 remote signer + const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner) + + // Wait for signer to be ready (may require user approval) + await remoteSigner.blockUntilReady() + + // Set signer on NDK instance + ndk.signer = remoteSigner + + // Get authenticated user + const user = await remoteSigner.user() + + console.log('โœ… Logged in via NIP-46:', user.npub) + + // Store local signer key for reconnection + return { + user, + signer: remoteSigner, + localSignerKey: localSigner.privateKey + } + } catch (error) { + console.error('โŒ NIP-46 login failed:', error) + throw error + } +} + +// ============================================================ +// AUTO-LOGIN FROM LOCAL STORAGE +// ============================================================ + +const STORAGE_KEYS = { + AUTO_LOGIN: 'nostr:auto-login', + LOCAL_SIGNER: 'nostr:local-signer', + BUNKER_URL: 'nostr:bunker-url', + ENCRYPTED_KEY: 'nostr:encrypted-key' +} + +const getAuthFromStorage = async (ndk: NDK) => { + try { + // Check if auto-login is enabled + const autoLogin = localStorage.getItem(STORAGE_KEYS.AUTO_LOGIN) + if (autoLogin !== 'true') { + return null + } + + // Try NIP-46 bunker connection + const privateKey = localStorage.getItem(STORAGE_KEYS.LOCAL_SIGNER) + const bunkerUrl = localStorage.getItem(STORAGE_KEYS.BUNKER_URL) + + if (privateKey && bunkerUrl) { + return await loginWithNip46(ndk, bunkerUrl, privateKey) + } + + // Try encrypted private key + const encryptedKey = localStorage.getItem(STORAGE_KEYS.ENCRYPTED_KEY) + if (encryptedKey) { + // Would need decryption password from user + return { needsPassword: true, encryptedKey } + } + + // Fallback to extension + return await loginWithExtension(ndk) + } catch (error) { + console.error('Auto-login failed:', error) + return null + } +} + +// ============================================================ +// SAVE AUTH TO STORAGE +// ============================================================ + +const saveAuthToStorage = ( + method: 'extension' | 'private-key' | 'nip46', + data?: { + privateKey?: string + bunkerUrl?: string + encryptedKey?: string + } +) => { + // Enable auto-login + localStorage.setItem(STORAGE_KEYS.AUTO_LOGIN, 'true') + + if (method === 'nip46' && data?.privateKey && data?.bunkerUrl) { + localStorage.setItem(STORAGE_KEYS.LOCAL_SIGNER, data.privateKey) + localStorage.setItem(STORAGE_KEYS.BUNKER_URL, data.bunkerUrl) + } else if (method === 'private-key' && data?.encryptedKey) { + localStorage.setItem(STORAGE_KEYS.ENCRYPTED_KEY, data.encryptedKey) + } + // Extension doesn't need storage +} + +// ============================================================ +// LOGOUT +// ============================================================ + +const logout = (ndk: NDK) => { + // Remove signer from NDK + ndk.signer = undefined + + // Clear all auth storage + Object.values(STORAGE_KEYS).forEach(key => { + localStorage.removeItem(key) + }) + + console.log('โœ… Logged out successfully') +} + +// ============================================================ +// GET CURRENT USER +// ============================================================ + +const getCurrentUser = async (ndk: NDK) => { + if (!ndk.signer) { + return null + } + + try { + const user = await ndk.signer.user() + return { + pubkey: user.pubkey, + npub: user.npub, + profile: await user.fetchProfile() + } + } catch (error) { + console.error('Failed to get current user:', error) + return null + } +} + +// ============================================================ +// USAGE EXAMPLE +// ============================================================ + +async function authExample(ndk: NDK) { + // Try auto-login first + let auth = await getAuthFromStorage(ndk) + + if (!auth) { + // Manual login options + console.log('Choose login method:') + console.log('1. Browser Extension (NIP-07)') + console.log('2. Private Key') + console.log('3. Remote Signer (NIP-46)') + + // Example: login with extension + auth = await loginWithExtension(ndk) + saveAuthToStorage('extension') + } + + if (auth && 'needsPassword' in auth) { + // Handle encrypted key case + console.log('Password required for encrypted key') + return + } + + // Get current user info + const currentUser = await getCurrentUser(ndk) + console.log('Current user:', currentUser) + + // Logout when done + // logout(ndk) +} + +export { + loginWithExtension, + loginWithPrivateKey, + loginWithNip46, + getAuthFromStorage, + saveAuthToStorage, + logout, + getCurrentUser +} + diff --git a/.claude/skills/ndk/examples/03-publishing-events.ts b/.claude/skills/ndk/examples/03-publishing-events.ts new file mode 100644 index 0000000..bd068e4 --- /dev/null +++ b/.claude/skills/ndk/examples/03-publishing-events.ts @@ -0,0 +1,376 @@ +/** + * NDK Event Publishing Patterns + * + * Examples from: src/publish/orders.tsx, scripts/gen_products.ts + */ + +import NDK, { NDKEvent, NDKTag } from '@nostr-dev-kit/ndk' + +// ============================================================ +// BASIC EVENT PUBLISHING +// ============================================================ + +const publishBasicNote = async (ndk: NDK, content: string) => { + // Create event + const event = new NDKEvent(ndk) + event.kind = 1 // Text note + event.content = content + event.tags = [] + + // Sign and publish + await event.sign() + await event.publish() + + console.log('โœ… Published note:', event.id) + return event.id +} + +// ============================================================ +// EVENT WITH TAGS +// ============================================================ + +const publishNoteWithTags = async ( + ndk: NDK, + content: string, + options: { + mentions?: string[] // pubkeys to mention + hashtags?: string[] + replyTo?: string // event ID + } +) => { + const event = new NDKEvent(ndk) + event.kind = 1 + event.content = content + event.tags = [] + + // Add mentions + if (options.mentions) { + options.mentions.forEach(pubkey => { + event.tags.push(['p', pubkey]) + }) + } + + // Add hashtags + if (options.hashtags) { + options.hashtags.forEach(tag => { + event.tags.push(['t', tag]) + }) + } + + // Add reply + if (options.replyTo) { + event.tags.push(['e', options.replyTo, '', 'reply']) + } + + await event.sign() + await event.publish() + + return event.id +} + +// ============================================================ +// PRODUCT LISTING (PARAMETERIZED REPLACEABLE EVENT) +// ============================================================ + +interface ProductData { + slug: string // Unique identifier + title: string + description: string + price: number + currency: string + images: string[] + shippingRefs?: string[] + category?: string +} + +const publishProduct = async (ndk: NDK, product: ProductData) => { + const event = new NDKEvent(ndk) + event.kind = 30402 // Product listing kind + event.content = product.description + + // Build tags + event.tags = [ + ['d', product.slug], // Unique identifier (required for replaceable) + ['title', product.title], + ['price', product.price.toString(), product.currency], + ] + + // Add images + product.images.forEach(image => { + event.tags.push(['image', image]) + }) + + // Add shipping options + if (product.shippingRefs) { + product.shippingRefs.forEach(ref => { + event.tags.push(['shipping', ref]) + }) + } + + // Add category + if (product.category) { + event.tags.push(['t', product.category]) + } + + // Optional: set custom timestamp + event.created_at = Math.floor(Date.now() / 1000) + + await event.sign() + await event.publish() + + console.log('โœ… Published product:', product.title) + return event.id +} + +// ============================================================ +// ORDER CREATION EVENT +// ============================================================ + +interface OrderData { + orderId: string + sellerPubkey: string + productRef: string + quantity: number + totalAmount: string + currency: string + shippingRef?: string + shippingAddress?: string + email?: string + phone?: string + notes?: string +} + +const createOrder = async (ndk: NDK, order: OrderData) => { + const event = new NDKEvent(ndk) + event.kind = 16 // Order processing kind + event.content = order.notes || '' + + // Required tags per spec + event.tags = [ + ['p', order.sellerPubkey], + ['subject', `Order ${order.orderId.substring(0, 8)}`], + ['type', 'order-creation'], + ['order', order.orderId], + ['amount', order.totalAmount], + ['item', order.productRef, order.quantity.toString()], + ] + + // Optional tags + if (order.shippingRef) { + event.tags.push(['shipping', order.shippingRef]) + } + + if (order.shippingAddress) { + event.tags.push(['address', order.shippingAddress]) + } + + if (order.email) { + event.tags.push(['email', order.email]) + } + + if (order.phone) { + event.tags.push(['phone', order.phone]) + } + + try { + await event.sign() + await event.publish() + + console.log('โœ… Order created:', order.orderId) + return { success: true, eventId: event.id } + } catch (error) { + console.error('โŒ Failed to create order:', error) + return { success: false, error } + } +} + +// ============================================================ +// STATUS UPDATE EVENT +// ============================================================ + +const publishStatusUpdate = async ( + ndk: NDK, + orderId: string, + recipientPubkey: string, + status: 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled', + notes?: string +) => { + const event = new NDKEvent(ndk) + event.kind = 16 + event.content = notes || `Order status updated to ${status}` + event.tags = [ + ['p', recipientPubkey], + ['subject', 'order-info'], + ['type', 'status-update'], + ['order', orderId], + ['status', status], + ] + + await event.sign() + await event.publish() + + return event.id +} + +// ============================================================ +// BATCH PUBLISHING +// ============================================================ + +const publishMultipleEvents = async ( + ndk: NDK, + events: Array<{ kind: number; content: string; tags: NDKTag[] }> +) => { + const results = [] + + for (const eventData of events) { + try { + const event = new NDKEvent(ndk) + event.kind = eventData.kind + event.content = eventData.content + event.tags = eventData.tags + + await event.sign() + await event.publish() + + results.push({ success: true, eventId: event.id }) + } catch (error) { + results.push({ success: false, error }) + } + } + + return results +} + +// ============================================================ +// PUBLISH WITH CUSTOM SIGNER +// ============================================================ + +import { NDKSigner } from '@nostr-dev-kit/ndk' + +const publishWithCustomSigner = async ( + ndk: NDK, + signer: NDKSigner, + eventData: { kind: number; content: string; tags: NDKTag[] } +) => { + const event = new NDKEvent(ndk) + event.kind = eventData.kind + event.content = eventData.content + event.tags = eventData.tags + + // Sign with specific signer (not ndk.signer) + await event.sign(signer) + await event.publish() + + return event.id +} + +// ============================================================ +// ERROR HANDLING PATTERN +// ============================================================ + +const publishWithErrorHandling = async ( + ndk: NDK, + eventData: { kind: number; content: string; tags: NDKTag[] } +) => { + // Validate NDK + if (!ndk) { + throw new Error('NDK not initialized') + } + + // Validate signer + if (!ndk.signer) { + throw new Error('No active signer. Please login first.') + } + + try { + const event = new NDKEvent(ndk) + event.kind = eventData.kind + event.content = eventData.content + event.tags = eventData.tags + + // Sign + await event.sign() + + // Verify signature + if (!event.sig) { + throw new Error('Event signing failed') + } + + // Publish + await event.publish() + + // Verify event ID + if (!event.id) { + throw new Error('Event ID not generated') + } + + return { + success: true, + eventId: event.id, + pubkey: event.pubkey + } + } catch (error) { + console.error('Publishing failed:', error) + + if (error instanceof Error) { + // Handle specific error types + if (error.message.includes('relay')) { + throw new Error('Failed to publish to relays. Check connection.') + } + if (error.message.includes('sign')) { + throw new Error('Failed to sign event. Check signer.') + } + } + + throw error + } +} + +// ============================================================ +// USAGE EXAMPLE +// ============================================================ + +async function publishingExample(ndk: NDK) { + // Simple note + await publishBasicNote(ndk, 'Hello Nostr!') + + // Note with tags + await publishNoteWithTags(ndk, 'Check out this product!', { + hashtags: ['marketplace', 'nostr'], + mentions: ['pubkey123...'] + }) + + // Product listing + await publishProduct(ndk, { + slug: 'bitcoin-tshirt', + title: 'Bitcoin T-Shirt', + description: 'High quality Bitcoin t-shirt', + price: 25, + currency: 'USD', + images: ['https://example.com/image.jpg'], + category: 'clothing' + }) + + // Order + await createOrder(ndk, { + orderId: 'order-123', + sellerPubkey: 'seller-pubkey', + productRef: '30402:pubkey:bitcoin-tshirt', + quantity: 1, + totalAmount: '25.00', + currency: 'USD', + email: 'customer@example.com' + }) +} + +export { + publishBasicNote, + publishNoteWithTags, + publishProduct, + createOrder, + publishStatusUpdate, + publishMultipleEvents, + publishWithCustomSigner, + publishWithErrorHandling +} + diff --git a/.claude/skills/ndk/examples/04-querying-subscribing.ts b/.claude/skills/ndk/examples/04-querying-subscribing.ts new file mode 100644 index 0000000..ff75e97 --- /dev/null +++ b/.claude/skills/ndk/examples/04-querying-subscribing.ts @@ -0,0 +1,404 @@ +/** + * NDK Query and Subscription Patterns + * + * Examples from: src/queries/orders.tsx, src/queries/payment.tsx + */ + +import NDK, { NDKEvent, NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk' + +// ============================================================ +// BASIC FETCH (ONE-TIME QUERY) +// ============================================================ + +const fetchNotes = async (ndk: NDK, authorPubkey: string, limit: number = 50) => { + const filter: NDKFilter = { + kinds: [1], // Text notes + authors: [authorPubkey], + limit + } + + // Fetch returns a Set + const events = await ndk.fetchEvents(filter) + + // Convert to array and sort by timestamp + const eventArray = Array.from(events).sort((a, b) => + (b.created_at || 0) - (a.created_at || 0) + ) + + return eventArray +} + +// ============================================================ +// FETCH WITH MULTIPLE FILTERS +// ============================================================ + +const fetchProductsByMultipleAuthors = async ( + ndk: NDK, + pubkeys: string[] +) => { + const filter: NDKFilter = { + kinds: [30402], // Product listings + authors: pubkeys, + limit: 100 + } + + const events = await ndk.fetchEvents(filter) + return Array.from(events) +} + +// ============================================================ +// FETCH WITH TAG FILTERS +// ============================================================ + +const fetchOrderEvents = async (ndk: NDK, orderId: string) => { + const filter: NDKFilter = { + kinds: [16, 17], // Order and payment receipt + '#order': [orderId], // Tag filter (note the # prefix) + } + + const events = await ndk.fetchEvents(filter) + return Array.from(events) +} + +// ============================================================ +// FETCH WITH TIME RANGE +// ============================================================ + +const fetchRecentEvents = async ( + ndk: NDK, + kind: number, + hoursAgo: number = 24 +) => { + const now = Math.floor(Date.now() / 1000) + const since = now - (hoursAgo * 3600) + + const filter: NDKFilter = { + kinds: [kind], + since, + until: now, + limit: 100 + } + + const events = await ndk.fetchEvents(filter) + return Array.from(events) +} + +// ============================================================ +// FETCH BY EVENT ID +// ============================================================ + +const fetchEventById = async (ndk: NDK, eventId: string) => { + const filter: NDKFilter = { + ids: [eventId] + } + + const events = await ndk.fetchEvents(filter) + + if (events.size === 0) { + return null + } + + return Array.from(events)[0] +} + +// ============================================================ +// BASIC SUBSCRIPTION (REAL-TIME) +// ============================================================ + +const subscribeToNotes = ( + ndk: NDK, + authorPubkey: string, + onEvent: (event: NDKEvent) => void +): NDKSubscription => { + const filter: NDKFilter = { + kinds: [1], + authors: [authorPubkey] + } + + const subscription = ndk.subscribe(filter, { + closeOnEose: false // Keep open for real-time updates + }) + + // Event handler + subscription.on('event', (event: NDKEvent) => { + onEvent(event) + }) + + // EOSE (End of Stored Events) handler + subscription.on('eose', () => { + console.log('โœ… Received all stored events') + }) + + return subscription +} + +// ============================================================ +// SUBSCRIPTION WITH CLEANUP +// ============================================================ + +const createManagedSubscription = ( + ndk: NDK, + filter: NDKFilter, + handlers: { + onEvent: (event: NDKEvent) => void + onEose?: () => void + onClose?: () => void + } +) => { + const subscription = ndk.subscribe(filter, { closeOnEose: false }) + + subscription.on('event', handlers.onEvent) + + if (handlers.onEose) { + subscription.on('eose', handlers.onEose) + } + + if (handlers.onClose) { + subscription.on('close', handlers.onClose) + } + + // Return cleanup function + return () => { + subscription.stop() + console.log('โœ… Subscription stopped') + } +} + +// ============================================================ +// MONITORING SPECIFIC EVENT +// ============================================================ + +const monitorPaymentReceipt = ( + ndk: NDK, + orderId: string, + invoiceId: string, + onPaymentReceived: (preimage: string) => void +): NDKSubscription => { + const sessionStart = Math.floor(Date.now() / 1000) + + const filter: NDKFilter = { + kinds: [17], // Payment receipt + '#order': [orderId], + '#payment-request': [invoiceId], + since: sessionStart - 30 // 30 second buffer for clock skew + } + + const subscription = ndk.subscribe(filter, { closeOnEose: false }) + + subscription.on('event', (event: NDKEvent) => { + // Verify event is recent + if (event.created_at && event.created_at < sessionStart - 30) { + console.log('โฐ Ignoring old receipt') + return + } + + // Verify it's the correct invoice + const paymentRequestTag = event.tags.find(tag => tag[0] === 'payment-request') + if (paymentRequestTag?.[1] !== invoiceId) { + return + } + + // Extract preimage + const paymentTag = event.tags.find(tag => tag[0] === 'payment') + const preimage = paymentTag?.[3] || 'external-payment' + + console.log('โœ… Payment received!') + subscription.stop() + onPaymentReceived(preimage) + }) + + return subscription +} + +// ============================================================ +// REACT INTEGRATION PATTERN +// ============================================================ + +import { useEffect, useState } from 'react' + +function useOrderSubscription(ndk: NDK | null, orderId: string) { + const [events, setEvents] = useState([]) + const [eosed, setEosed] = useState(false) + + useEffect(() => { + if (!ndk || !orderId) return + + const filter: NDKFilter = { + kinds: [16, 17], + '#order': [orderId] + } + + const subscription = ndk.subscribe(filter, { closeOnEose: false }) + + subscription.on('event', (event: NDKEvent) => { + setEvents(prev => { + // Avoid duplicates + if (prev.some(e => e.id === event.id)) { + return prev + } + return [...prev, event].sort((a, b) => + (a.created_at || 0) - (b.created_at || 0) + ) + }) + }) + + subscription.on('eose', () => { + setEosed(true) + }) + + // Cleanup on unmount + return () => { + subscription.stop() + } + }, [ndk, orderId]) + + return { events, eosed } +} + +// ============================================================ +// REACT QUERY INTEGRATION +// ============================================================ + +import { useQuery, useQueryClient } from '@tanstack/react-query' + +// Query function +const fetchProducts = async (ndk: NDK, pubkey: string) => { + if (!ndk) throw new Error('NDK not initialized') + + const filter: NDKFilter = { + kinds: [30402], + authors: [pubkey] + } + + const events = await ndk.fetchEvents(filter) + return Array.from(events) +} + +// Hook with subscription for real-time updates +function useProductsWithSubscription(ndk: NDK | null, pubkey: string) { + const queryClient = useQueryClient() + + // Initial query + const query = useQuery({ + queryKey: ['products', pubkey], + queryFn: () => fetchProducts(ndk!, pubkey), + enabled: !!ndk && !!pubkey, + staleTime: 30000 + }) + + // Real-time subscription + useEffect(() => { + if (!ndk || !pubkey) return + + const filter: NDKFilter = { + kinds: [30402], + authors: [pubkey] + } + + const subscription = ndk.subscribe(filter, { closeOnEose: false }) + + subscription.on('event', () => { + // Invalidate query to trigger refetch + queryClient.invalidateQueries({ queryKey: ['products', pubkey] }) + }) + + return () => { + subscription.stop() + } + }, [ndk, pubkey, queryClient]) + + return query +} + +// ============================================================ +// ADVANCED: WAITING FOR SPECIFIC EVENT +// ============================================================ + +const waitForEvent = ( + ndk: NDK, + filter: NDKFilter, + condition: (event: NDKEvent) => boolean, + timeoutMs: number = 30000 +): Promise => { + return new Promise((resolve) => { + const subscription = ndk.subscribe(filter, { closeOnEose: false }) + + // Timeout + const timeout = setTimeout(() => { + subscription.stop() + resolve(null) + }, timeoutMs) + + // Event handler + subscription.on('event', (event: NDKEvent) => { + if (condition(event)) { + clearTimeout(timeout) + subscription.stop() + resolve(event) + } + }) + }) +} + +// Usage example +async function waitForPayment(ndk: NDK, orderId: string, invoiceId: string) { + const paymentEvent = await waitForEvent( + ndk, + { + kinds: [17], + '#order': [orderId], + since: Math.floor(Date.now() / 1000) + }, + (event) => { + const tag = event.tags.find(t => t[0] === 'payment-request') + return tag?.[1] === invoiceId + }, + 60000 // 60 second timeout + ) + + if (paymentEvent) { + console.log('โœ… Payment confirmed!') + return paymentEvent + } else { + console.log('โฐ Payment timeout') + return null + } +} + +// ============================================================ +// USAGE EXAMPLES +// ============================================================ + +async function queryExample(ndk: NDK) { + // Fetch notes + const notes = await fetchNotes(ndk, 'pubkey123', 50) + console.log(`Found ${notes.length} notes`) + + // Subscribe to new notes + const cleanup = subscribeToNotes(ndk, 'pubkey123', (event) => { + console.log('New note:', event.content) + }) + + // Clean up after 60 seconds + setTimeout(cleanup, 60000) + + // Monitor payment + monitorPaymentReceipt(ndk, 'order-123', 'invoice-456', (preimage) => { + console.log('Payment received:', preimage) + }) +} + +export { + fetchNotes, + fetchProductsByMultipleAuthors, + fetchOrderEvents, + fetchRecentEvents, + fetchEventById, + subscribeToNotes, + createManagedSubscription, + monitorPaymentReceipt, + useOrderSubscription, + useProductsWithSubscription, + waitForEvent +} + diff --git a/.claude/skills/ndk/examples/05-users-profiles.ts b/.claude/skills/ndk/examples/05-users-profiles.ts new file mode 100644 index 0000000..3a9beb6 --- /dev/null +++ b/.claude/skills/ndk/examples/05-users-profiles.ts @@ -0,0 +1,423 @@ +/** + * NDK User and Profile Handling + * + * Examples from: src/queries/profiles.tsx, src/components/Profile.tsx + */ + +import NDK, { NDKUser, NDKUserProfile } from '@nostr-dev-kit/ndk' +import { nip19 } from 'nostr-tools' + +// ============================================================ +// FETCH PROFILE BY NPUB +// ============================================================ + +const fetchProfileByNpub = async (ndk: NDK, npub: string): Promise => { + try { + // Get user object from npub + const user = ndk.getUser({ npub }) + + // Fetch profile from relays + const profile = await user.fetchProfile() + + return profile + } catch (error) { + console.error('Failed to fetch profile:', error) + return null + } +} + +// ============================================================ +// FETCH PROFILE BY HEX PUBKEY +// ============================================================ + +const fetchProfileByPubkey = async (ndk: NDK, pubkey: string): Promise => { + try { + const user = ndk.getUser({ hexpubkey: pubkey }) + const profile = await user.fetchProfile() + + return profile + } catch (error) { + console.error('Failed to fetch profile:', error) + return null + } +} + +// ============================================================ +// FETCH PROFILE BY NIP-05 +// ============================================================ + +const fetchProfileByNip05 = async (ndk: NDK, nip05: string): Promise => { + try { + // Resolve NIP-05 identifier to user + const user = await ndk.getUserFromNip05(nip05) + + if (!user) { + console.log('User not found for NIP-05:', nip05) + return null + } + + // Fetch profile + const profile = await user.fetchProfile() + + return profile + } catch (error) { + console.error('Failed to fetch profile by NIP-05:', error) + return null + } +} + +// ============================================================ +// FETCH PROFILE BY ANY IDENTIFIER +// ============================================================ + +const fetchProfileByIdentifier = async ( + ndk: NDK, + identifier: string +): Promise<{ profile: NDKUserProfile | null; user: NDKUser | null }> => { + try { + // Check if it's a NIP-05 (contains @) + if (identifier.includes('@')) { + const user = await ndk.getUserFromNip05(identifier) + if (!user) return { profile: null, user: null } + + const profile = await user.fetchProfile() + return { profile, user } + } + + // Check if it's an npub + if (identifier.startsWith('npub')) { + const user = ndk.getUser({ npub: identifier }) + const profile = await user.fetchProfile() + return { profile, user } + } + + // Assume it's a hex pubkey + const user = ndk.getUser({ hexpubkey: identifier }) + const profile = await user.fetchProfile() + return { profile, user } + } catch (error) { + console.error('Failed to fetch profile:', error) + return { profile: null, user: null } + } +} + +// ============================================================ +// GET CURRENT USER +// ============================================================ + +const getCurrentUser = async (ndk: NDK): Promise => { + if (!ndk.signer) { + console.log('No signer set') + return null + } + + try { + const user = await ndk.signer.user() + return user + } catch (error) { + console.error('Failed to get current user:', error) + return null + } +} + +// ============================================================ +// PROFILE DATA STRUCTURE +// ============================================================ + +interface ProfileData { + // Standard fields + name?: string + displayName?: string + display_name?: string + picture?: string + image?: string + banner?: string + about?: string + + // Contact + nip05?: string + lud06?: string // LNURL + lud16?: string // Lightning address + + // Social + website?: string + + // Raw data + [key: string]: any +} + +// ============================================================ +// EXTRACT PROFILE INFO +// ============================================================ + +const extractProfileInfo = (profile: NDKUserProfile | null) => { + if (!profile) { + return { + displayName: 'Anonymous', + avatar: null, + bio: null, + lightningAddress: null, + nip05: null + } + } + + return { + displayName: profile.displayName || profile.display_name || profile.name || 'Anonymous', + avatar: profile.picture || profile.image || null, + banner: profile.banner || null, + bio: profile.about || null, + lightningAddress: profile.lud16 || profile.lud06 || null, + nip05: profile.nip05 || null, + website: profile.website || null + } +} + +// ============================================================ +// UPDATE PROFILE +// ============================================================ + +import { NDKEvent } from '@nostr-dev-kit/ndk' + +const updateProfile = async (ndk: NDK, profileData: Partial) => { + if (!ndk.signer) { + throw new Error('No signer available') + } + + // Get current profile + const currentUser = await ndk.signer.user() + const currentProfile = await currentUser.fetchProfile() + + // Merge with new data + const updatedProfile = { + ...currentProfile, + ...profileData + } + + // Create kind 0 (metadata) event + const event = new NDKEvent(ndk) + event.kind = 0 + event.content = JSON.stringify(updatedProfile) + event.tags = [] + + await event.sign() + await event.publish() + + console.log('โœ… Profile updated') + return event.id +} + +// ============================================================ +// BATCH FETCH PROFILES +// ============================================================ + +const fetchMultipleProfiles = async ( + ndk: NDK, + pubkeys: string[] +): Promise> => { + const profiles = new Map() + + // Fetch all profiles in parallel + await Promise.all( + pubkeys.map(async (pubkey) => { + try { + const user = ndk.getUser({ hexpubkey: pubkey }) + const profile = await user.fetchProfile() + profiles.set(pubkey, profile) + } catch (error) { + console.error(`Failed to fetch profile for ${pubkey}:`, error) + profiles.set(pubkey, null) + } + }) + ) + + return profiles +} + +// ============================================================ +// CONVERT BETWEEN FORMATS +// ============================================================ + +const convertPubkeyFormats = (identifier: string) => { + try { + // If it's npub, convert to hex + if (identifier.startsWith('npub')) { + const decoded = nip19.decode(identifier) + if (decoded.type === 'npub') { + return { + hex: decoded.data as string, + npub: identifier + } + } + } + + // If it's hex, convert to npub + if (/^[0-9a-f]{64}$/.test(identifier)) { + return { + hex: identifier, + npub: nip19.npubEncode(identifier) + } + } + + throw new Error('Invalid pubkey format') + } catch (error) { + console.error('Format conversion failed:', error) + return null + } +} + +// ============================================================ +// REACT HOOK FOR PROFILE +// ============================================================ + +import { useQuery } from '@tanstack/react-query' +import { useEffect, useState } from 'react' + +function useProfile(ndk: NDK | null, npub: string | undefined) { + return useQuery({ + queryKey: ['profile', npub], + queryFn: async () => { + if (!ndk || !npub) throw new Error('NDK or npub missing') + return await fetchProfileByNpub(ndk, npub) + }, + enabled: !!ndk && !!npub, + staleTime: 5 * 60 * 1000, // 5 minutes + cacheTime: 30 * 60 * 1000 // 30 minutes + }) +} + +// ============================================================ +// REACT COMPONENT EXAMPLE +// ============================================================ + +interface ProfileDisplayProps { + ndk: NDK + pubkey: string +} + +function ProfileDisplay({ ndk, pubkey }: ProfileDisplayProps) { + const [profile, setProfile] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const loadProfile = async () => { + setLoading(true) + try { + const user = ndk.getUser({ hexpubkey: pubkey }) + const fetchedProfile = await user.fetchProfile() + setProfile(fetchedProfile) + } catch (error) { + console.error('Failed to load profile:', error) + } finally { + setLoading(false) + } + } + + loadProfile() + }, [ndk, pubkey]) + + if (loading) { + return
Loading profile...
+ } + + const info = extractProfileInfo(profile) + + return ( +
+ {info.avatar && {info.displayName}} +

{info.displayName}

+ {info.bio &&

{info.bio}

} + {info.nip05 && โœ“ {info.nip05}} + {info.lightningAddress && โšก {info.lightningAddress}} +
+ ) +} + +// ============================================================ +// FOLLOW/UNFOLLOW USER +// ============================================================ + +const followUser = async (ndk: NDK, pubkeyToFollow: string) => { + if (!ndk.signer) { + throw new Error('No signer available') + } + + // Fetch current contact list (kind 3) + const currentUser = await ndk.signer.user() + const contactListFilter = { + kinds: [3], + authors: [currentUser.pubkey] + } + + const existingEvents = await ndk.fetchEvents(contactListFilter) + const existingContactList = existingEvents.size > 0 + ? Array.from(existingEvents)[0] + : null + + // Get existing p tags + const existingPTags = existingContactList + ? existingContactList.tags.filter(tag => tag[0] === 'p') + : [] + + // Check if already following + const alreadyFollowing = existingPTags.some(tag => tag[1] === pubkeyToFollow) + if (alreadyFollowing) { + console.log('Already following this user') + return + } + + // Create new contact list with added user + const event = new NDKEvent(ndk) + event.kind = 3 + event.content = existingContactList?.content || '' + event.tags = [ + ...existingPTags, + ['p', pubkeyToFollow] + ] + + await event.sign() + await event.publish() + + console.log('โœ… Now following user') +} + +// ============================================================ +// USAGE EXAMPLE +// ============================================================ + +async function profileExample(ndk: NDK) { + // Fetch by different identifiers + const profile1 = await fetchProfileByNpub(ndk, 'npub1...') + const profile2 = await fetchProfileByNip05(ndk, 'user@domain.com') + const profile3 = await fetchProfileByPubkey(ndk, 'hex pubkey...') + + // Extract display info + const info = extractProfileInfo(profile1) + console.log('Display name:', info.displayName) + console.log('Avatar:', info.avatar) + + // Update own profile + await updateProfile(ndk, { + name: 'My Name', + about: 'My bio', + picture: 'https://example.com/avatar.jpg', + lud16: 'me@getalby.com' + }) + + // Follow someone + await followUser(ndk, 'pubkey to follow') +} + +export { + fetchProfileByNpub, + fetchProfileByPubkey, + fetchProfileByNip05, + fetchProfileByIdentifier, + getCurrentUser, + extractProfileInfo, + updateProfile, + fetchMultipleProfiles, + convertPubkeyFormats, + useProfile, + followUser +} + diff --git a/.claude/skills/ndk/examples/README.md b/.claude/skills/ndk/examples/README.md new file mode 100644 index 0000000..25b990a --- /dev/null +++ b/.claude/skills/ndk/examples/README.md @@ -0,0 +1,94 @@ +# NDK Examples Index + +Complete code examples extracted from the Plebeian Market production codebase. + +## Available Examples + +### 01-initialization.ts +- Basic NDK initialization +- Multiple NDK instances (main + zap relays) +- Connection with timeout protection +- Connection status checking +- Full initialization flow with error handling + +### 02-authentication.ts +- NIP-07 browser extension login +- Private key signer +- NIP-46 remote signer (Bunker) +- Auto-login from localStorage +- Saving auth credentials +- Logout functionality +- Getting current user + +### 03-publishing-events.ts +- Basic note publishing +- Events with tags (mentions, hashtags, replies) +- Product listings (parameterized replaceable events) +- Order creation events +- Status update events +- Batch publishing +- Custom signer usage +- Comprehensive error handling + +### 04-querying-subscribing.ts +- Basic fetch queries +- Multiple author queries +- Tag filtering +- Time range filtering +- Event ID lookup +- Real-time subscriptions +- Subscription cleanup patterns +- React integration hooks +- React Query integration +- Waiting for specific events +- Payment monitoring + +### 05-users-profiles.ts +- Fetch profile by npub +- Fetch profile by hex pubkey +- Fetch profile by NIP-05 +- Universal identifier lookup +- Get current user +- Extract profile information +- Update user profile +- Batch fetch multiple profiles +- Convert between pubkey formats (hex/npub) +- React hooks for profiles +- Follow/unfollow users + +## Usage + +Each file contains: +- Fully typed TypeScript code +- JSDoc comments explaining the pattern +- Error handling examples +- Integration patterns with React/TanStack Query +- Real-world usage examples + +All examples are based on actual production code from the Plebeian Market application. + +## Running Examples + +```typescript +import { initializeNDK } from './01-initialization' +import { loginWithExtension } from './02-authentication' +import { publishBasicNote } from './03-publishing-events' + +// Initialize NDK +const { ndk, isConnected } = await initializeNDK() + +if (isConnected) { + // Authenticate + const { user } = await loginWithExtension(ndk) + + // Publish + await publishBasicNote(ndk, 'Hello Nostr!') +} +``` + +## Additional Resources + +- See `../ndk-skill.md` for detailed documentation +- See `../quick-reference.md` for quick lookup +- Check the main codebase for more complex patterns + diff --git a/.claude/skills/ndk/ndk-skill.md b/.claude/skills/ndk/ndk-skill.md new file mode 100644 index 0000000..680fb43 --- /dev/null +++ b/.claude/skills/ndk/ndk-skill.md @@ -0,0 +1,701 @@ +# NDK (Nostr Development Kit) - Claude Skill Reference + +## Overview + +NDK is the primary Nostr development kit with outbox-model support, designed for building Nostr applications with TypeScript/JavaScript. This reference is based on analyzing production usage in the Plebeian Market codebase. + +## Core Concepts + +### 1. NDK Initialization + +**Basic Pattern:** +```typescript +import NDK from '@nostr-dev-kit/ndk' + +// Simple initialization +const ndk = new NDK({ + explicitRelayUrls: ['wss://relay.damus.io', 'wss://relay.nostr.band'] +}) + +await ndk.connect() +``` + +**Store-based Pattern (Production):** +```typescript +// From src/lib/stores/ndk.ts +const ndk = new NDK({ + explicitRelayUrls: relays || defaultRelaysUrls, +}) + +// Separate NDK for zaps on specialized relays +const zapNdk = new NDK({ + explicitRelayUrls: ZAP_RELAYS, +}) + +// Connect with timeout protection +const connectPromise = ndk.connect() +const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Connection timeout')), timeoutMs) +) +await Promise.race([connectPromise, timeoutPromise]) +``` + +### 2. Authentication & Signers + +NDK supports multiple signer types for different authentication methods: + +#### NIP-07 (Browser Extension) +```typescript +import { NDKNip07Signer } from '@nostr-dev-kit/ndk' + +const signer = new NDKNip07Signer() +await signer.blockUntilReady() +ndk.signer = signer + +const user = await signer.user() +``` + +#### Private Key Signer +```typescript +import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk' + +const signer = new NDKPrivateKeySigner(privateKeyHex) +await signer.blockUntilReady() +ndk.signer = signer + +const user = await signer.user() +``` + +#### NIP-46 (Remote Signer / Bunker) +```typescript +import { NDKNip46Signer } from '@nostr-dev-kit/ndk' + +const localSigner = new NDKPrivateKeySigner(localPrivateKey) +const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner) +await remoteSigner.blockUntilReady() +ndk.signer = remoteSigner + +const user = await remoteSigner.user() +``` + +**Key Points:** +- Always call `blockUntilReady()` before using a signer +- Store signer reference in your state management +- Set `ndk.signer` to enable signing operations +- Use `await signer.user()` to get the authenticated user + +### 3. Event Creation & Publishing + +#### Basic Event Pattern +```typescript +import { NDKEvent } from '@nostr-dev-kit/ndk' + +// Create event +const event = new NDKEvent(ndk) +event.kind = 1 // Kind 1 = text note +event.content = "Hello Nostr!" +event.tags = [ + ['t', 'nostr'], + ['p', recipientPubkey] +] + +// Sign and publish +await event.sign() // Uses ndk.signer automatically +await event.publish() + +// Get event ID after signing +console.log(event.id) +``` + +#### Production Pattern with Error Handling +```typescript +// From src/publish/orders.tsx +const event = new NDKEvent(ndk) +event.kind = ORDER_PROCESS_KIND +event.content = orderNotes || '' +event.tags = [ + ['p', sellerPubkey], + ['subject', `Order for ${productName}`], + ['type', 'order-creation'], + ['order', orderId], + ['amount', totalAmount], + ['item', productRef, quantity.toString()], +] + +// Optional tags +if (shippingRef) { + event.tags.push(['shipping', shippingRef]) +} + +try { + await event.sign(signer) // Can pass explicit signer + await event.publish() + return event.id +} catch (error) { + console.error('Failed to publish event:', error) + throw error +} +``` + +**Key Points:** +- Create event with `new NDKEvent(ndk)` +- Set `kind`, `content`, and `tags` properties +- Optional: Set `created_at` timestamp (defaults to now) +- Call `await event.sign()` before publishing +- Call `await event.publish()` to broadcast to relays +- Access `event.id` after signing for the event hash + +### 4. Querying Events with Filters + +#### fetchEvents() - One-time Fetch +```typescript +import { NDKFilter } from '@nostr-dev-kit/ndk' + +// Simple filter +const filter: NDKFilter = { + kinds: [30402], // Product listings + authors: [merchantPubkey], + limit: 50 +} + +const events = await ndk.fetchEvents(filter) +// Returns Set + +// Convert to array and process +const eventArray = Array.from(events) +const sortedEvents = eventArray.sort((a, b) => + (b.created_at || 0) - (a.created_at || 0) +) +``` + +#### Advanced Filters +```typescript +// Multiple kinds +const filter: NDKFilter = { + kinds: [16, 17], // Orders and payment receipts + '#order': [orderId], // Tag filter (# prefix) + since: Math.floor(Date.now() / 1000) - 86400, // Last 24 hours + limit: 100 +} + +// Event ID lookup +const filter: NDKFilter = { + ids: [eventIdHex], +} + +// Tag filtering +const filter: NDKFilter = { + kinds: [1], + '#p': [pubkey], // Events mentioning pubkey + '#t': ['nostr'], // Events with hashtag 'nostr' +} +``` + +### 5. Subscriptions (Real-time) + +#### Basic Subscription +```typescript +// From src/queries/blacklist.tsx +const filter = { + kinds: [10000], + authors: [appPubkey], +} + +const subscription = ndk.subscribe(filter, { + closeOnEose: false, // Keep open for real-time updates +}) + +subscription.on('event', (event: NDKEvent) => { + console.log('New event received:', event) + // Process event +}) + +subscription.on('eose', () => { + console.log('End of stored events') +}) + +// Cleanup +subscription.stop() +``` + +#### Production Pattern with React Query +```typescript +// From src/queries/orders.tsx +useEffect(() => { + if (!orderId || !ndk) return + + const filter = { + kinds: [ORDER_PROCESS_KIND, PAYMENT_RECEIPT_KIND], + '#order': [orderId], + } + + const subscription = ndk.subscribe(filter, { + closeOnEose: false, + }) + + subscription.on('event', (newEvent) => { + // Invalidate React Query cache to trigger refetch + queryClient.invalidateQueries({ + queryKey: orderKeys.details(orderId) + }) + }) + + // Cleanup on unmount + return () => { + subscription.stop() + } +}, [orderId, ndk, queryClient]) +``` + +#### Monitoring Specific Events +```typescript +// From src/queries/payment.tsx - Payment receipt monitoring +const receiptFilter = { + kinds: [17], // Payment receipts + '#order': [orderId], + '#payment-request': [invoiceId], + since: sessionStartTime - 30, // Clock skew buffer +} + +const subscription = ndk.subscribe(receiptFilter, { + closeOnEose: false, +}) + +subscription.on('event', (receiptEvent: NDKEvent) => { + // Verify this is the correct invoice + const paymentRequestTag = receiptEvent.tags.find( + tag => tag[0] === 'payment-request' + ) + + if (paymentRequestTag?.[1] === invoiceId) { + const paymentTag = receiptEvent.tags.find(tag => tag[0] === 'payment') + const preimage = paymentTag?.[3] || 'external-payment' + + // Stop subscription after finding payment + subscription.stop() + handlePaymentReceived(preimage) + } +}) +``` + +**Key Subscription Patterns:** +- Use `closeOnEose: false` for real-time monitoring +- Use `closeOnEose: true` for one-time historical fetch +- Always call `subscription.stop()` in cleanup +- Listen to both `'event'` and `'eose'` events +- Filter events in the handler for specific conditions +- Integrate with React Query for reactive UI updates + +### 6. User & Profile Handling + +#### Fetching User Profiles +```typescript +// From src/queries/profiles.tsx + +// By npub +const user = ndk.getUser({ npub }) +const profile = await user.fetchProfile() +// Returns NDKUserProfile with name, picture, about, etc. + +// By hex pubkey +const user = ndk.getUser({ hexpubkey: pubkey }) +const profile = await user.fetchProfile() + +// By NIP-05 identifier +const user = await ndk.getUserFromNip05('user@domain.com') +if (user) { + const profile = await user.fetchProfile() +} + +// Profile fields +const name = profile?.name || profile?.displayName +const avatar = profile?.picture || profile?.image +const bio = profile?.about +const nip05 = profile?.nip05 +const lud16 = profile?.lud16 // Lightning address +``` + +#### Getting Current User +```typescript +// Active user (authenticated) +const user = ndk.activeUser + +// From signer +const user = await ndk.signer?.user() + +// User properties +const pubkey = user.pubkey // Hex format +const npub = user.npub // NIP-19 encoded +``` + +### 7. NDK Event Object + +#### Essential Properties +```typescript +interface NDKEvent { + id: string // Event hash (after signing) + kind: number // Event kind + content: string // Event content + tags: NDKTag[] // Array of tag arrays + created_at?: number // Unix timestamp + pubkey?: string // Author pubkey (after signing) + sig?: string // Signature (after signing) + + // Methods + sign(signer?: NDKSigner): Promise + publish(): Promise + tagValue(tagName: string): string | undefined +} + +type NDKTag = string[] // e.g., ['p', pubkey, relay, petname] +``` + +#### Tag Helpers +```typescript +// Get first value of a tag +const orderId = event.tagValue('order') +const recipientPubkey = event.tagValue('p') + +// Find specific tag +const paymentTag = event.tags.find(tag => tag[0] === 'payment') +const preimage = paymentTag?.[3] + +// Get all tags of a type +const pTags = event.tags.filter(tag => tag[0] === 'p') +const allPubkeys = pTags.map(tag => tag[1]) + +// Common tag patterns +event.tags.push(['p', pubkey]) // Mention +event.tags.push(['e', eventId]) // Reference event +event.tags.push(['t', 'nostr']) // Hashtag +event.tags.push(['d', identifier]) // Replaceable event ID +event.tags.push(['a', '30402:pubkey:d-tag']) // Addressable event reference +``` + +### 8. Parameterized Replaceable Events (NIP-33) + +Used for products, collections, profiles that need updates: + +```typescript +// Product listing (kind 30402) +const event = new NDKEvent(ndk) +event.kind = 30402 +event.content = JSON.stringify(productDetails) +event.tags = [ + ['d', productSlug], // Unique identifier + ['title', productName], + ['price', price, currency], + ['image', imageUrl], + ['shipping', shippingRef], +] + +await event.sign() +await event.publish() + +// Querying replaceable events +const filter = { + kinds: [30402], + authors: [merchantPubkey], + '#d': [productSlug], // Specific product +} + +const events = await ndk.fetchEvents(filter) +// Returns only the latest version due to replaceable nature +``` + +### 9. Relay Management + +#### Getting Relay Status +```typescript +// From src/lib/stores/ndk.ts +const connectedRelays = Array.from(ndk.pool?.relays.values() || []) + .filter(relay => relay.status === 1) // 1 = connected + .map(relay => relay.url) + +const outboxRelays = Array.from(ndk.outboxPool?.relays.values() || []) +``` + +#### Adding Relays +```typescript +// Add explicit relays +ndk.addExplicitRelay('wss://relay.example.com') + +// Multiple relays +const relays = ['wss://relay1.com', 'wss://relay2.com'] +relays.forEach(url => ndk.addExplicitRelay(url)) +``` + +### 10. Common Patterns & Best Practices + +#### Null Safety +```typescript +// Always check NDK initialization +const ndk = ndkActions.getNDK() +if (!ndk) throw new Error('NDK not initialized') + +// Check signer before operations requiring auth +const signer = ndk.signer +if (!signer) throw new Error('No active signer') + +// Check user authentication +const user = ndk.activeUser +if (!user) throw new Error('Not authenticated') +``` + +#### Error Handling +```typescript +try { + const events = await ndk.fetchEvents(filter) + if (events.size === 0) { + return null // No results found + } + return Array.from(events) +} catch (error) { + console.error('Failed to fetch events:', error) + throw new Error('Could not fetch data from relays') +} +``` + +#### Connection Lifecycle +```typescript +// Initialize once at app startup +const ndk = new NDK({ explicitRelayUrls: relays }) + +// Connect with timeout +await Promise.race([ + ndk.connect(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), 10000) + ) +]) + +// Check connection status +const isConnected = ndk.pool?.connectedRelays().length > 0 + +// Reconnect if needed +if (!isConnected) { + await ndk.connect() +} +``` + +#### Subscription Cleanup +```typescript +// In React components +useEffect(() => { + if (!ndk) return + + const sub = ndk.subscribe(filter, { closeOnEose: false }) + + sub.on('event', handleEvent) + sub.on('eose', handleEose) + + // Critical: cleanup on unmount + return () => { + sub.stop() + } +}, [dependencies]) +``` + +#### Event Validation +```typescript +// Check required fields before processing +if (!event.pubkey) { + console.error('Event missing pubkey') + return +} + +if (!event.created_at) { + console.error('Event missing timestamp') + return +} + +// Verify event age +const now = Math.floor(Date.now() / 1000) +const eventAge = now - (event.created_at || 0) +if (eventAge > 86400) { // Older than 24 hours + console.log('Event is old, skipping') + return +} + +// Validate specific tags exist +const orderId = event.tagValue('order') +if (!orderId) { + console.error('Order event missing order ID') + return +} +``` + +### 11. Common Event Kinds + +```typescript +// NIP-01: Basic Events +const KIND_METADATA = 0 // User profile +const KIND_TEXT_NOTE = 1 // Short text note +const KIND_RECOMMEND_RELAY = 2 // Relay recommendation + +// NIP-04: Encrypted Direct Messages +const KIND_ENCRYPTED_DM = 4 + +// NIP-25: Reactions +const KIND_REACTION = 7 + +// NIP-51: Lists +const KIND_MUTE_LIST = 10000 +const KIND_PIN_LIST = 10001 +const KIND_RELAY_LIST = 10002 + +// NIP-57: Lightning Zaps +const KIND_ZAP_REQUEST = 9734 +const KIND_ZAP_RECEIPT = 9735 + +// Marketplace (Plebeian/Gamma spec) +const ORDER_PROCESS_KIND = 16 // Order processing +const PAYMENT_RECEIPT_KIND = 17 // Payment receipts +const DIRECT_MESSAGE_KIND = 14 // Direct messages +const ORDER_GENERAL_KIND = 27 // General order events +const SHIPPING_KIND = 30405 // Shipping options +const PRODUCT_KIND = 30402 // Product listings +const COLLECTION_KIND = 30401 // Product collections +const REVIEW_KIND = 30407 // Product reviews + +// Application Handlers +const APP_HANDLER_KIND = 31990 // NIP-89 app handlers +``` + +## Integration with TanStack Query + +NDK works excellently with TanStack Query for reactive data fetching: + +### Query Functions +```typescript +// From src/queries/products.tsx +export const fetchProductsByPubkey = async (pubkey: string) => { + const ndk = ndkActions.getNDK() + if (!ndk) throw new Error('NDK not initialized') + + const filter: NDKFilter = { + kinds: [30402], + authors: [pubkey], + } + + const events = await ndk.fetchEvents(filter) + return Array.from(events).map(parseProductEvent) +} + +export const useProductsByPubkey = (pubkey: string) => { + return useQuery({ + queryKey: productKeys.byAuthor(pubkey), + queryFn: () => fetchProductsByPubkey(pubkey), + enabled: !!pubkey, + staleTime: 30000, + }) +} +``` + +### Combining Queries with Subscriptions +```typescript +// Query for initial data +const { data: order, refetch } = useQuery({ + queryKey: orderKeys.details(orderId), + queryFn: () => fetchOrderById(orderId), + enabled: !!orderId, +}) + +// Subscription for real-time updates +useEffect(() => { + if (!orderId || !ndk) return + + const sub = ndk.subscribe( + { kinds: [16, 17], '#order': [orderId] }, + { closeOnEose: false } + ) + + sub.on('event', () => { + // Invalidate query to trigger refetch + queryClient.invalidateQueries({ + queryKey: orderKeys.details(orderId) + }) + }) + + return () => sub.stop() +}, [orderId, ndk, queryClient]) +``` + +## Troubleshooting + +### Events Not Received +- Check relay connections: `ndk.pool?.connectedRelays()` +- Verify filter syntax (especially tag filters with `#` prefix) +- Check event timestamps match filter's `since`/`until` +- Ensure `closeOnEose: false` for real-time subscriptions + +### Signing Errors +- Verify signer is initialized: `await signer.blockUntilReady()` +- Check signer is set: `ndk.signer !== undefined` +- For NIP-07, ensure browser extension is installed and enabled +- For NIP-46, verify bunker URL and local signer are correct + +### Connection Timeouts +- Implement connection timeout pattern shown above +- Try connecting to fewer, more reliable relays initially +- Use fallback relays in production + +### Duplicate Events +- NDK deduplicates by event ID automatically +- For subscriptions, track processed event IDs if needed +- Use replaceable events (kinds 10000-19999, 30000-39999) when appropriate + +## Performance Optimization + +### Batching Queries +```typescript +// Instead of multiple fetchEvents calls +const [products, orders, profiles] = await Promise.all([ + ndk.fetchEvents(productFilter), + ndk.fetchEvents(orderFilter), + ndk.fetchEvents(profileFilter), +]) +``` + +### Limiting Results +```typescript +const filter = { + kinds: [1], + authors: [pubkey], + limit: 50, // Limit results + since: recentTimestamp, // Only recent events +} +``` + +### Caching with React Query +```typescript +export const useProfile = (npub: string) => { + return useQuery({ + queryKey: profileKeys.byNpub(npub), + queryFn: () => fetchProfileByNpub(npub), + staleTime: 5 * 60 * 1000, // 5 minutes + cacheTime: 30 * 60 * 1000, // 30 minutes + enabled: !!npub, + }) +} +``` + +## References + +- **NDK GitHub**: https://github.com/nostr-dev-kit/ndk +- **NDK Documentation**: https://ndk.fyi +- **Nostr NIPs**: https://github.com/nostr-protocol/nips +- **Production Example**: Plebeian Market codebase + +## Key Files in This Codebase + +- `src/lib/stores/ndk.ts` - NDK store and initialization +- `src/lib/stores/auth.ts` - Authentication with NDK signers +- `src/queries/*.tsx` - Query patterns with NDK +- `src/publish/*.tsx` - Event publishing patterns +- `scripts/gen_*.ts` - Event creation examples + +--- + +*This reference is based on NDK version used in production and real-world patterns from the Plebeian Market application.* + diff --git a/.claude/skills/ndk/quick-reference.md b/.claude/skills/ndk/quick-reference.md new file mode 100644 index 0000000..3af6cc2 --- /dev/null +++ b/.claude/skills/ndk/quick-reference.md @@ -0,0 +1,351 @@ +# NDK Quick Reference + +Fast lookup guide for common NDK tasks. + +## Quick Start + +```typescript +import NDK from '@nostr-dev-kit/ndk' + +const ndk = new NDK({ explicitRelayUrls: ['wss://relay.damus.io'] }) +await ndk.connect() +``` + +## Authentication + +### Browser Extension (NIP-07) +```typescript +import { NDKNip07Signer } from '@nostr-dev-kit/ndk' +const signer = new NDKNip07Signer() +await signer.blockUntilReady() +ndk.signer = signer +``` + +### Private Key +```typescript +import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk' +const signer = new NDKPrivateKeySigner(privateKeyHex) +await signer.blockUntilReady() +ndk.signer = signer +``` + +### Remote Signer (NIP-46) +```typescript +import { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk' +const localSigner = new NDKPrivateKeySigner() +const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner) +await remoteSigner.blockUntilReady() +ndk.signer = remoteSigner +``` + +## Publish Event + +```typescript +import { NDKEvent } from '@nostr-dev-kit/ndk' + +const event = new NDKEvent(ndk) +event.kind = 1 +event.content = "Hello Nostr!" +event.tags = [['t', 'nostr']] + +await event.sign() +await event.publish() +``` + +## Query Events (One-time) + +```typescript +const events = await ndk.fetchEvents({ + kinds: [1], + authors: [pubkey], + limit: 50 +}) + +// Convert Set to Array +const eventArray = Array.from(events) +``` + +## Subscribe (Real-time) + +```typescript +const sub = ndk.subscribe( + { kinds: [1], authors: [pubkey] }, + { closeOnEose: false } +) + +sub.on('event', (event) => { + console.log('New event:', event.content) +}) + +// Cleanup +sub.stop() +``` + +## Get User Profile + +```typescript +// By npub +const user = ndk.getUser({ npub }) +const profile = await user.fetchProfile() + +// By hex pubkey +const user = ndk.getUser({ hexpubkey: pubkey }) +const profile = await user.fetchProfile() + +// By NIP-05 +const user = await ndk.getUserFromNip05('user@domain.com') +const profile = await user?.fetchProfile() +``` + +## Common Filters + +```typescript +// By author +{ kinds: [1], authors: [pubkey] } + +// By tag +{ kinds: [1], '#p': [pubkey] } +{ kinds: [30402], '#d': [productSlug] } + +// By time +{ + kinds: [1], + since: Math.floor(Date.now() / 1000) - 86400, // Last 24h + until: Math.floor(Date.now() / 1000) +} + +// By event ID +{ ids: [eventId] } + +// Multiple conditions +{ + kinds: [16, 17], + '#order': [orderId], + since: timestamp, + limit: 100 +} +``` + +## Tag Helpers + +```typescript +// Get first tag value +const orderId = event.tagValue('order') + +// Find specific tag +const tag = event.tags.find(t => t[0] === 'payment') +const value = tag?.[1] + +// Get all of one type +const pTags = event.tags.filter(t => t[0] === 'p') + +// Common tag formats +['p', pubkey] // Mention +['e', eventId] // Event reference +['t', 'nostr'] // Hashtag +['d', identifier] // Replaceable ID +['a', '30402:pubkey:d-tag'] // Addressable reference +``` + +## Error Handling Pattern + +```typescript +const ndk = ndkActions.getNDK() +if (!ndk) throw new Error('NDK not initialized') + +const signer = ndk.signer +if (!signer) throw new Error('No active signer') + +try { + await event.publish() +} catch (error) { + console.error('Publish failed:', error) + throw error +} +``` + +## React Integration + +```typescript +// Query function +export const fetchProducts = async (pubkey: string) => { + const ndk = ndkActions.getNDK() + if (!ndk) throw new Error('NDK not initialized') + + const events = await ndk.fetchEvents({ + kinds: [30402], + authors: [pubkey] + }) + + return Array.from(events) +} + +// React Query hook +export const useProducts = (pubkey: string) => { + return useQuery({ + queryKey: ['products', pubkey], + queryFn: () => fetchProducts(pubkey), + enabled: !!pubkey, + }) +} + +// Subscription in useEffect +useEffect(() => { + if (!ndk || !orderId) return + + const sub = ndk.subscribe( + { kinds: [16], '#order': [orderId] }, + { closeOnEose: false } + ) + + sub.on('event', () => { + queryClient.invalidateQueries(['order', orderId]) + }) + + return () => sub.stop() +}, [ndk, orderId, queryClient]) +``` + +## Common Event Kinds + +```typescript +0 // Metadata (profile) +1 // Text note +4 // Encrypted DM (NIP-04) +7 // Reaction +9735 // Zap receipt +10000 // Mute list +10002 // Relay list +30402 // Product listing (Marketplace) +31990 // App handler (NIP-89) +``` + +## Relay Management + +```typescript +// Check connection +const connected = ndk.pool?.connectedRelays().length > 0 + +// Get connected relays +const relays = Array.from(ndk.pool?.relays.values() || []) + .filter(r => r.status === 1) + +// Add relay +ndk.addExplicitRelay('wss://relay.example.com') +``` + +## Connection with Timeout + +```typescript +const connectWithTimeout = async (timeoutMs = 10000) => { + const connectPromise = ndk.connect() + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), timeoutMs) + ) + + await Promise.race([connectPromise, timeoutPromise]) +} +``` + +## Current User + +```typescript +// Active user +const user = ndk.activeUser + +// From signer +const user = await ndk.signer?.user() + +// User info +const pubkey = user.pubkey // hex +const npub = user.npub // NIP-19 +``` + +## Parameterized Replaceable Events + +```typescript +// Create +const event = new NDKEvent(ndk) +event.kind = 30402 +event.content = JSON.stringify(data) +event.tags = [ + ['d', uniqueIdentifier], // Required for replaceable + ['title', 'Product Name'], +] + +await event.sign() +await event.publish() + +// Query (returns latest only) +const events = await ndk.fetchEvents({ + kinds: [30402], + authors: [pubkey], + '#d': [identifier] +}) +``` + +## Validation Checks + +```typescript +// Event age check +const now = Math.floor(Date.now() / 1000) +const age = now - (event.created_at || 0) +if (age > 86400) console.log('Event older than 24h') + +// Required fields +if (!event.pubkey || !event.created_at || !event.sig) { + throw new Error('Invalid event') +} + +// Tag existence +const orderId = event.tagValue('order') +if (!orderId) throw new Error('Missing order tag') +``` + +## Performance Tips + +```typescript +// Batch queries +const [products, orders] = await Promise.all([ + ndk.fetchEvents(productFilter), + ndk.fetchEvents(orderFilter) +]) + +// Limit results +const filter = { + kinds: [1], + limit: 50, + since: recentTimestamp +} + +// Cache with React Query +const { data } = useQuery({ + queryKey: ['profile', npub], + queryFn: () => fetchProfile(npub), + staleTime: 5 * 60 * 1000, // 5 min +}) +``` + +## Debugging + +```typescript +// Check NDK state +console.log('Connected:', ndk.pool?.connectedRelays()) +console.log('Signer:', ndk.signer) +console.log('Active user:', ndk.activeUser) + +// Event inspection +console.log('Event ID:', event.id) +console.log('Tags:', event.tags) +console.log('Content:', event.content) +console.log('Author:', event.pubkey) + +// Subscription events +sub.on('event', e => console.log('Event:', e)) +sub.on('eose', () => console.log('End of stored events')) +``` + +--- + +For detailed explanations and advanced patterns, see `ndk-skill.md`. + diff --git a/.claude/skills/ndk/troubleshooting.md b/.claude/skills/ndk/troubleshooting.md new file mode 100644 index 0000000..ac98fec --- /dev/null +++ b/.claude/skills/ndk/troubleshooting.md @@ -0,0 +1,530 @@ +# NDK Common Patterns & Troubleshooting + +Quick reference for common patterns and solutions to frequent NDK issues. + +## Common Patterns + +### Store-Based NDK Management + +```typescript +// Store pattern (recommended for React apps) +import { Store } from '@tanstack/store' + +interface NDKState { + ndk: NDK | null + isConnected: boolean + signer?: NDKSigner +} + +const ndkStore = new Store({ + ndk: null, + isConnected: false +}) + +export const ndkActions = { + initialize: () => { + const ndk = new NDK({ explicitRelayUrls: relays }) + ndkStore.setState({ ndk }) + return ndk + }, + + getNDK: () => ndkStore.state.ndk, + + setSigner: (signer: NDKSigner) => { + const ndk = ndkStore.state.ndk + if (ndk) { + ndk.signer = signer + ndkStore.setState({ signer }) + } + } +} +``` + +### Query + Subscription Pattern + +```typescript +// Initial data load + real-time updates +function useOrdersWithRealtime(orderId: string) { + const queryClient = useQueryClient() + const ndk = ndkActions.getNDK() + + // Fetch initial data + const query = useQuery({ + queryKey: ['orders', orderId], + queryFn: () => fetchOrders(orderId), + }) + + // Subscribe to updates + useEffect(() => { + if (!ndk || !orderId) return + + const sub = ndk.subscribe( + { kinds: [16], '#order': [orderId] }, + { closeOnEose: false } + ) + + sub.on('event', () => { + queryClient.invalidateQueries(['orders', orderId]) + }) + + return () => sub.stop() + }, [ndk, orderId]) + + return query +} +``` + +### Event Parsing Pattern + +```typescript +// Parse event tags into structured data +function parseProductEvent(event: NDKEvent) { + const getTag = (name: string) => + event.tags.find(t => t[0] === name)?.[1] + + const getAllTags = (name: string) => + event.tags.filter(t => t[0] === name).map(t => t[1]) + + return { + id: event.id, + slug: getTag('d'), + title: getTag('title'), + price: parseFloat(getTag('price') || '0'), + currency: event.tags.find(t => t[0] === 'price')?.[2] || 'USD', + images: getAllTags('image'), + shipping: getAllTags('shipping'), + description: event.content, + createdAt: event.created_at, + author: event.pubkey + } +} +``` + +### Relay Pool Pattern + +```typescript +// Separate NDK instances for different purposes +const mainNdk = new NDK({ + explicitRelayUrls: ['wss://relay.damus.io', 'wss://nos.lol'] +}) + +const zapNdk = new NDK({ + explicitRelayUrls: ['wss://relay.damus.io'] // Zap-optimized relays +}) + +const blossomNdk = new NDK({ + explicitRelayUrls: ['wss://blossom.server.com'] // Media server +}) + +await Promise.all([ + mainNdk.connect(), + zapNdk.connect(), + blossomNdk.connect() +]) +``` + +## Troubleshooting + +### Problem: Events Not Received + +**Symptoms:** Subscription doesn't receive events, fetchEvents returns empty Set + +**Solutions:** + +1. Check relay connection: +```typescript +const status = ndk.pool?.connectedRelays() +console.log('Connected relays:', status?.length) +if (status?.length === 0) { + await ndk.connect() +} +``` + +2. Verify filter syntax (especially tags): +```typescript +// โŒ Wrong +{ kinds: [16], 'order': [orderId] } + +// โœ… Correct (note the # prefix for tags) +{ kinds: [16], '#order': [orderId] } +``` + +3. Check timestamps: +```typescript +// Events might be too old/new +const now = Math.floor(Date.now() / 1000) +const filter = { + kinds: [1], + since: now - 86400, // Last 24 hours + until: now +} +``` + +4. Ensure closeOnEose is correct: +```typescript +// For real-time updates +ndk.subscribe(filter, { closeOnEose: false }) + +// For one-time historical fetch +ndk.subscribe(filter, { closeOnEose: true }) +``` + +### Problem: "NDK not initialized" + +**Symptoms:** `ndk` is null/undefined + +**Solutions:** + +1. Initialize before use: +```typescript +// In app entry point +const ndk = new NDK({ explicitRelayUrls: relays }) +await ndk.connect() +``` + +2. Add null checks: +```typescript +const ndk = ndkActions.getNDK() +if (!ndk) throw new Error('NDK not initialized') +``` + +3. Use initialization guard: +```typescript +const ensureNDK = () => { + let ndk = ndkActions.getNDK() + if (!ndk) { + ndk = ndkActions.initialize() + } + return ndk +} +``` + +### Problem: "No active signer" / Cannot Sign Events + +**Symptoms:** Event signing fails, publishing throws error + +**Solutions:** + +1. Check signer is set: +```typescript +if (!ndk.signer) { + throw new Error('Please login first') +} +``` + +2. Ensure blockUntilReady called: +```typescript +const signer = new NDKNip07Signer() +await signer.blockUntilReady() // โ† Critical! +ndk.signer = signer +``` + +3. Handle NIP-07 unavailable: +```typescript +try { + const signer = new NDKNip07Signer() + await signer.blockUntilReady() + ndk.signer = signer +} catch (error) { + console.error('Browser extension not available') + // Fallback to other auth method +} +``` + +### Problem: Duplicate Events in Subscriptions + +**Symptoms:** Same event received multiple times + +**Solutions:** + +1. Track processed event IDs: +```typescript +const processedIds = new Set() + +sub.on('event', (event) => { + if (processedIds.has(event.id)) return + processedIds.add(event.id) + handleEvent(event) +}) +``` + +2. Use Map for event storage: +```typescript +const [events, setEvents] = useState>(new Map()) + +sub.on('event', (event) => { + setEvents(prev => new Map(prev).set(event.id, event)) +}) +``` + +### Problem: Connection Timeout + +**Symptoms:** connect() hangs, never resolves + +**Solutions:** + +1. Use timeout wrapper: +```typescript +const connectWithTimeout = async (ndk: NDK, ms = 10000) => { + await Promise.race([ + ndk.connect(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), ms) + ) + ]) +} +``` + +2. Try fewer relays: +```typescript +// Start with reliable relays only +const reliableRelays = ['wss://relay.damus.io'] +const ndk = new NDK({ explicitRelayUrls: reliableRelays }) +``` + +3. Add connection retry: +```typescript +const connectWithRetry = async (ndk: NDK, maxRetries = 3) => { + for (let i = 0; i < maxRetries; i++) { + try { + await connectWithTimeout(ndk, 10000) + return + } catch (error) { + console.log(`Retry ${i + 1}/${maxRetries}`) + if (i === maxRetries - 1) throw error + } + } +} +``` + +### Problem: Subscription Memory Leak + +**Symptoms:** App gets slower, memory usage increases + +**Solutions:** + +1. Always stop subscriptions: +```typescript +useEffect(() => { + const sub = ndk.subscribe(filter, { closeOnEose: false }) + + // โ† CRITICAL: cleanup + return () => { + sub.stop() + } +}, [dependencies]) +``` + +2. Track active subscriptions: +```typescript +const activeSubscriptions = new Set() + +const createSub = (filter: NDKFilter) => { + const sub = ndk.subscribe(filter, { closeOnEose: false }) + activeSubscriptions.add(sub) + return sub +} + +const stopAllSubs = () => { + activeSubscriptions.forEach(sub => sub.stop()) + activeSubscriptions.clear() +} +``` + +### Problem: Profile Not Found + +**Symptoms:** fetchProfile() returns null/undefined + +**Solutions:** + +1. Check different relays: +```typescript +// Add more relay URLs +const ndk = new NDK({ + explicitRelayUrls: [ + 'wss://relay.damus.io', + 'wss://relay.nostr.band', + 'wss://nos.lol' + ] +}) +``` + +2. Verify pubkey format: +```typescript +// Ensure correct format +if (pubkey.startsWith('npub')) { + const user = ndk.getUser({ npub: pubkey }) +} else if (/^[0-9a-f]{64}$/.test(pubkey)) { + const user = ndk.getUser({ hexpubkey: pubkey }) +} +``` + +3. Handle missing profiles gracefully: +```typescript +const profile = await user.fetchProfile() +const displayName = profile?.name || profile?.displayName || 'Anonymous' +const avatar = profile?.picture || '/default-avatar.png' +``` + +### Problem: Events Published But Not Visible + +**Symptoms:** publish() succeeds but event not found in queries + +**Solutions:** + +1. Verify event was signed: +```typescript +await event.sign() +console.log('Event ID:', event.id) // Should be set +console.log('Signature:', event.sig) // Should exist +``` + +2. Check relay acceptance: +```typescript +const relays = await event.publish() +console.log('Published to relays:', relays) +``` + +3. Query immediately after publish: +```typescript +await event.publish() + +// Wait a moment for relay propagation +await new Promise(resolve => setTimeout(resolve, 1000)) + +const found = await ndk.fetchEvents({ ids: [event.id] }) +console.log('Event found:', found.size > 0) +``` + +### Problem: NIP-46 Connection Fails + +**Symptoms:** Remote signer connection times out or fails + +**Solutions:** + +1. Verify bunker URL format: +```typescript +// Correct format: bunker://?relay=wss://... +const isValidBunkerUrl = (url: string) => { + return url.startsWith('bunker://') && url.includes('?relay=') +} +``` + +2. Ensure local signer is ready: +```typescript +const localSigner = new NDKPrivateKeySigner(privateKey) +await localSigner.blockUntilReady() + +const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner) +await remoteSigner.blockUntilReady() +``` + +3. Store credentials for reconnection: +```typescript +// Save for future sessions +localStorage.setItem('local-signer-key', localSigner.privateKey) +localStorage.setItem('bunker-url', bunkerUrl) +``` + +## Performance Tips + +### Optimize Queries + +```typescript +// โŒ Slow: Multiple sequential queries +const products = await ndk.fetchEvents({ kinds: [30402], authors: [pk1] }) +const orders = await ndk.fetchEvents({ kinds: [16], authors: [pk1] }) +const profiles = await ndk.fetchEvents({ kinds: [0], authors: [pk1] }) + +// โœ… Fast: Parallel queries +const [products, orders, profiles] = await Promise.all([ + ndk.fetchEvents({ kinds: [30402], authors: [pk1] }), + ndk.fetchEvents({ kinds: [16], authors: [pk1] }), + ndk.fetchEvents({ kinds: [0], authors: [pk1] }) +]) +``` + +### Cache Profile Lookups + +```typescript +const profileCache = new Map() + +const getCachedProfile = async (ndk: NDK, pubkey: string) => { + if (profileCache.has(pubkey)) { + return profileCache.get(pubkey)! + } + + const user = ndk.getUser({ hexpubkey: pubkey }) + const profile = await user.fetchProfile() + if (profile) { + profileCache.set(pubkey, profile) + } + + return profile +} +``` + +### Limit Result Sets + +```typescript +// Always use limit to prevent over-fetching +const filter: NDKFilter = { + kinds: [1], + authors: [pubkey], + limit: 50 // โ† Important! +} +``` + +### Debounce Subscription Updates + +```typescript +import { debounce } from 'lodash' + +const debouncedUpdate = debounce((event: NDKEvent) => { + handleEvent(event) +}, 300) + +sub.on('event', debouncedUpdate) +``` + +## Testing Tips + +### Mock NDK in Tests + +```typescript +const mockNDK = { + fetchEvents: vi.fn().mockResolvedValue(new Set()), + subscribe: vi.fn().mockReturnValue({ + on: vi.fn(), + stop: vi.fn() + }), + signer: { + user: vi.fn().mockResolvedValue({ pubkey: 'test-pubkey' }) + } +} as unknown as NDK +``` + +### Test Event Creation + +```typescript +const createTestEvent = (overrides?: Partial): NDKEvent => { + return { + id: 'test-id', + kind: 1, + content: 'test content', + tags: [], + created_at: Math.floor(Date.now() / 1000), + pubkey: 'test-pubkey', + sig: 'test-sig', + ...overrides + } as NDKEvent +} +``` + +--- + +For more detailed information, see: +- `ndk-skill.md` - Complete reference +- `quick-reference.md` - Quick lookup +- `examples/` - Code examples + diff --git a/.claude/skills/react/README.md b/.claude/skills/react/README.md new file mode 100644 index 0000000..9144da8 --- /dev/null +++ b/.claude/skills/react/README.md @@ -0,0 +1,119 @@ +# React 19 Skill + +A comprehensive Claude skill for working with React 19, including hooks, components, server components, and modern React architecture. + +## Contents + +### Main Skill File +- **SKILL.md** - Main skill document with React 19 fundamentals, hooks, components, and best practices + +### References +- **hooks-quick-reference.md** - Quick reference for all React hooks with examples +- **server-components.md** - Complete guide to React Server Components and Server Functions +- **performance.md** - Performance optimization strategies and techniques + +### Examples +- **practical-patterns.tsx** - Real-world React patterns and solutions + +## What This Skill Covers + +### Core Topics +- React 19 features and improvements +- All built-in hooks (useState, useEffect, useTransition, useOptimistic, etc.) +- Component patterns and composition +- Server Components and Server Functions +- React Compiler and automatic optimization +- Performance optimization techniques +- Form handling and validation +- Error boundaries and error handling +- Context and global state management +- Code splitting and lazy loading + +### Best Practices +- Component design principles +- State management strategies +- Performance optimization +- Error handling patterns +- TypeScript integration +- Testing considerations +- Accessibility guidelines + +## When to Use This Skill + +Use this skill when: +- Building React 19 applications +- Working with React hooks +- Implementing server components +- Optimizing React performance +- Troubleshooting React-specific issues +- Understanding concurrent features +- Working with forms and user input +- Implementing complex UI patterns + +## Quick Start Examples + +### Basic Component +```typescript +interface ButtonProps { + label: string + onClick: () => void +} + +const Button = ({ label, onClick }: ButtonProps) => { + return +} +``` + +### Using Hooks +```typescript +const Counter = () => { + const [count, setCount] = useState(0) + + useEffect(() => { + console.log(`Count is: ${count}`) + }, [count]) + + return ( + + ) +} +``` + +### Server Component +```typescript +const Page = async () => { + const data = await fetchData() + return
{data}
+} +``` + +### Server Function +```typescript +'use server' + +export async function createUser(formData: FormData) { + const name = formData.get('name') + return await db.user.create({ data: { name } }) +} +``` + +## Related Skills + +- **typescript** - TypeScript patterns for React +- **ndk** - Nostr integration with React +- **skill-creator** - Creating reusable component libraries + +## Resources + +- [React Documentation](https://react.dev) +- [React API Reference](https://react.dev/reference/react) +- [React Hooks Reference](https://react.dev/reference/react/hooks) +- [React Server Components](https://react.dev/reference/rsc) +- [React Compiler](https://react.dev/reference/react-compiler) + +## Version + +This skill is based on React 19.2 and includes the latest features and APIs. + diff --git a/.claude/skills/react/SKILL.md b/.claude/skills/react/SKILL.md new file mode 100644 index 0000000..abe826f --- /dev/null +++ b/.claude/skills/react/SKILL.md @@ -0,0 +1,1026 @@ +--- +name: react +description: This skill should be used when working with React 19, including hooks, components, server components, concurrent features, and React DOM APIs. Provides comprehensive knowledge of React patterns, best practices, and modern React architecture. +--- + +# React 19 Skill + +This skill provides comprehensive knowledge and patterns for working with React 19 effectively in modern applications. + +## When to Use This Skill + +Use this skill when: +- Building React applications with React 19 features +- Working with React hooks and component patterns +- Implementing server components and server functions +- Using concurrent features and transitions +- Optimizing React application performance +- Troubleshooting React-specific issues +- Working with React DOM APIs and client/server rendering +- Using React Compiler features + +## Core Concepts + +### React 19 Overview + +React 19 introduces significant improvements: +- **Server Components** - Components that render on the server +- **Server Functions** - Functions that run on the server from client code +- **Concurrent Features** - Better performance with concurrent rendering +- **React Compiler** - Automatic memoization and optimization +- **Form Actions** - Built-in form handling with useActionState +- **Improved Hooks** - New hooks like useOptimistic, useActionState +- **Better Hydration** - Improved SSR and hydration performance + +### Component Fundamentals + +Use functional components with hooks: + +```typescript +// Functional component with props interface +interface ButtonProps { + label: string + onClick: () => void + variant?: 'primary' | 'secondary' +} + +const Button = ({ label, onClick, variant = 'primary' }: ButtonProps) => { + return ( + + ) +} +``` + +**Key Principles:** +- Use functional components over class components +- Define prop interfaces in TypeScript +- Use destructuring for props +- Provide default values for optional props +- Keep components focused and composable + +## React Hooks Reference + +### State Hooks + +#### useState +Manage local component state: + +```typescript +const [count, setCount] = useState(0) +const [user, setUser] = useState(null) + +// Named return variables pattern +const handleIncrement = () => { + setCount(prev => prev + 1) // Functional update +} + +// Update object state immutably +setUser(prev => prev ? { ...prev, name: 'New Name' } : null) +``` + +#### useReducer +Manage complex state with reducer pattern: + +```typescript +type State = { count: number; status: 'idle' | 'loading' } +type Action = + | { type: 'increment' } + | { type: 'decrement' } + | { type: 'setStatus'; status: State['status'] } + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case 'increment': + return { ...state, count: state.count + 1 } + case 'decrement': + return { ...state, count: state.count - 1 } + case 'setStatus': + return { ...state, status: action.status } + default: + return state + } +} + +const [state, dispatch] = useReducer(reducer, { count: 0, status: 'idle' }) +``` + +#### useActionState +Handle form actions with pending states (React 19): + +```typescript +const [state, formAction, isPending] = useActionState( + async (previousState: FormState, formData: FormData) => { + const name = formData.get('name') as string + + // Server action or async operation + const result = await saveUser({ name }) + + return { success: true, data: result } + }, + { success: false, data: null } +) + +return ( +
+ + +
+) +``` + +### Effect Hooks + +#### useEffect +Run side effects after render: + +```typescript +// Named return variables preferred +useEffect(() => { + const controller = new AbortController() + + const fetchData = async () => { + const response = await fetch('/api/data', { + signal: controller.signal + }) + const data = await response.json() + setData(data) + } + + fetchData() + + // Cleanup function + return () => { + controller.abort() + } +}, [dependencies]) // Dependencies array +``` + +**Key Points:** +- Always return cleanup function for subscriptions +- Use dependency array correctly to avoid infinite loops +- Don't forget to handle race conditions with AbortController +- Effects run after paint, not during render + +#### useLayoutEffect +Run effects synchronously after DOM mutations but before paint: + +```typescript +useLayoutEffect(() => { + // Measure DOM nodes + const height = ref.current?.getBoundingClientRect().height + setHeight(height) +}, []) +``` + +Use when you need to: +- Measure DOM layout +- Synchronously re-render before browser paints +- Prevent visual flicker + +#### useInsertionEffect +Insert styles before any DOM reads (for CSS-in-JS libraries): + +```typescript +useInsertionEffect(() => { + const style = document.createElement('style') + style.textContent = '.my-class { color: red; }' + document.head.appendChild(style) + + return () => { + document.head.removeChild(style) + } +}, []) +``` + +### Performance Hooks + +#### useMemo +Memoize expensive calculations: + +```typescript +const expensiveValue = useMemo(() => { + return computeExpensiveValue(a, b) +}, [a, b]) +``` + +**When to use:** +- Expensive calculations that would slow down renders +- Creating stable object references for dependency arrays +- Optimizing child component re-renders + +**When NOT to use:** +- Simple calculations (overhead not worth it) +- Values that change frequently + +#### useCallback +Memoize callback functions: + +```typescript +const handleClick = useCallback(() => { + console.log('Clicked', value) +}, [value]) + +// Pass to child that uses memo + +``` + +**Use when:** +- Passing callbacks to optimized child components +- Function is a dependency in another hook +- Function is used in effect cleanup + +### Ref Hooks + +#### useRef +Store mutable values that don't trigger re-renders: + +```typescript +// DOM reference +const inputRef = useRef(null) + +useEffect(() => { + inputRef.current?.focus() +}, []) + +// Mutable value storage +const countRef = useRef(0) +countRef.current += 1 // Doesn't trigger re-render +``` + +#### useImperativeHandle +Customize ref handle for parent components: + +```typescript +interface InputHandle { + focus: () => void + clear: () => void +} + +const CustomInput = forwardRef((props, ref) => { + const inputRef = useRef(null) + + useImperativeHandle(ref, () => ({ + focus: () => { + inputRef.current?.focus() + }, + clear: () => { + if (inputRef.current) { + inputRef.current.value = '' + } + } + })) + + return +}) +``` + +### Context Hooks + +#### useContext +Access context values: + +```typescript +// Create context +interface ThemeContext { + theme: 'light' | 'dark' + toggleTheme: () => void +} + +const ThemeContext = createContext(null) + +// Provider +const ThemeProvider = ({ children }: { children: React.ReactNode }) => { + const [theme, setTheme] = useState<'light' | 'dark'>('light') + + const toggleTheme = useCallback(() => { + setTheme(prev => prev === 'light' ? 'dark' : 'light') + }, []) + + return ( + + {children} + + ) +} + +// Consumer +const ThemedButton = () => { + const context = useContext(ThemeContext) + if (!context) throw new Error('useTheme must be used within ThemeProvider') + + const { theme, toggleTheme } = context + + return ( + + ) +} +``` + +### Transition Hooks + +#### useTransition +Mark state updates as non-urgent: + +```typescript +const [isPending, startTransition] = useTransition() + +const handleTabChange = (newTab: string) => { + startTransition(() => { + setTab(newTab) // Non-urgent update + }) +} + +return ( + <> + + {isPending && } + + +) +``` + +**Use for:** +- Marking expensive updates as non-urgent +- Keeping UI responsive during state transitions +- Preventing loading states for quick updates + +#### useDeferredValue +Defer re-rendering for non-urgent updates: + +```typescript +const [query, setQuery] = useState('') +const deferredQuery = useDeferredValue(query) + +// Use deferred value for expensive rendering +const results = useMemo(() => { + return searchResults(deferredQuery) +}, [deferredQuery]) + +return ( + <> + setQuery(e.target.value)} /> + + +) +``` + +### Optimistic Updates + +#### useOptimistic +Show optimistic state while async operation completes (React 19): + +```typescript +const [optimisticMessages, addOptimisticMessage] = useOptimistic( + messages, + (state, newMessage: string) => [ + ...state, + { id: 'temp', text: newMessage, pending: true } + ] +) + +const handleSend = async (formData: FormData) => { + const message = formData.get('message') as string + + // Show optimistic update immediately + addOptimisticMessage(message) + + // Send to server + await sendMessage(message) +} + +return ( + <> + {optimisticMessages.map(msg => ( +
+ {msg.text} +
+ ))} +
+ + +
+ +) +``` + +### Other Hooks + +#### useId +Generate unique IDs for accessibility: + +```typescript +const id = useId() + +return ( + <> + + + +) +``` + +#### useSyncExternalStore +Subscribe to external stores: + +```typescript +const subscribe = (callback: () => void) => { + store.subscribe(callback) + return () => store.unsubscribe(callback) +} + +const getSnapshot = () => store.getState() +const getServerSnapshot = () => store.getInitialState() + +const state = useSyncExternalStore( + subscribe, + getSnapshot, + getServerSnapshot +) +``` + +#### useDebugValue +Display custom label in React DevTools: + +```typescript +const useCustomHook = (value: string) => { + useDebugValue(value ? `Active: ${value}` : 'Inactive') + return value +} +``` + +## React Components + +### Fragment +Group elements without extra DOM nodes: + +```typescript +// Short syntax +<> + + + + +// Full syntax (when you need key prop) + +
{item.term}
+
{item.description}
+
+``` + +### Suspense +Show fallback while loading: + +```typescript +}> + + + +// With error boundary +}> + }> + + + +``` + +### StrictMode +Enable additional checks in development: + +```typescript + + + +``` + +**StrictMode checks:** +- Warns about deprecated APIs +- Detects unexpected side effects +- Highlights potential problems +- Double-invokes functions to catch bugs + +### Profiler +Measure rendering performance: + +```typescript + + + + +const onRender = ( + id: string, + phase: 'mount' | 'update', + actualDuration: number, + baseDuration: number, + startTime: number, + commitTime: number +) => { + console.log(`${id} took ${actualDuration}ms`) +} +``` + +## React APIs + +### memo +Prevent unnecessary re-renders: + +```typescript +const ExpensiveComponent = memo(({ data }: Props) => { + return
{data}
+}, (prevProps, nextProps) => { + // Return true if props are equal (skip render) + return prevProps.data === nextProps.data +}) +``` + +### lazy +Code-split components: + +```typescript +const Dashboard = lazy(() => import('./Dashboard')) + +}> + + +``` + +### startTransition +Mark updates as transitions imperatively: + +```typescript +startTransition(() => { + setTab('profile') +}) +``` + +### cache (React Server Components) +Cache function results per request: + +```typescript +const getUser = cache(async (id: string) => { + return await db.user.findUnique({ where: { id } }) +}) +``` + +### use (React 19) +Read context or promises in render: + +```typescript +// Read context +const theme = use(ThemeContext) + +// Read promise (must be wrapped in Suspense) +const data = use(fetchDataPromise) +``` + +## Server Components & Server Functions + +### Server Components + +Components that run only on the server: + +```typescript +// app/page.tsx (Server Component by default) +const Page = async () => { + // Can fetch data directly + const posts = await db.post.findMany() + + return ( +
+ {posts.map(post => ( + + ))} +
+ ) +} + +export default Page +``` + +**Benefits:** +- Direct database access +- Zero bundle size for server-only code +- Automatic code splitting +- Better performance + +### Server Functions + +Functions that run on server, callable from client: + +```typescript +'use server' + +export async function createPost(formData: FormData) { + const title = formData.get('title') as string + const content = formData.get('content') as string + + const post = await db.post.create({ + data: { title, content } + }) + + revalidatePath('/posts') + return post +} +``` + +**Usage from client:** + +```typescript +'use client' + +import { createPost } from './actions' + +const PostForm = () => { + const [state, formAction] = useActionState(createPost, null) + + return ( +
+ +