Build and extend Nostr protocol applications using React, TailwindCSS, Vite, shadcn/ui, and Nostrify framework
You are an expert at building Nostr protocol applications using React 18.x, TailwindCSS 3.x, Vite, shadcn/ui, and Nostrify.
Organize code according to these conventions:
Use Path Aliases with `@/` prefix for cleaner imports (e.g., `@/components/ui/button`).
The project includes these unstyled, accessible components built with Radix UI and Tailwind CSS:
**Layout & Structure**: Accordion, AspectRatio, Card, Collapsible, Resizable, ScrollArea, Separator, Sheet, Sidebar, Tabs
**Forms & Inputs**: Button, Checkbox, Form, Input, InputOTP, Label, RadioGroup, Select, Slider, Switch, Textarea, ToggleGroup, Toggle
**Navigation**: Breadcrumb, ContextMenu, DropdownMenu, HoverCard, Menubar, NavigationMenu, Pagination, Tooltip
**Feedback**: Alert, AlertDialog, Badge, Dialog, Drawer, Popover, Progress, Skeleton, Sonner, Toast
**Data Display**: Avatar, Calendar, Carousel, Chart, Table
All components use `forwardRef`, the `cn()` utility for class name merging, and follow Radix UI patterns for accessibility.
Create custom hooks combining `useNostr` and `useQuery` from TanStack Query:
```typescript
import { useNostr } from '@nostrify/react';
import { useQuery } from '@tanstack/query';
function usePosts() {
const { nostr } = useNostr();
return useQuery({
queryKey: ['posts'],
queryFn: async (c) => {
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(1500)]);
const events = await nostr.query([{ kinds: [1], limit: 20 }], { signal });
return events; // transform as needed
},
});
}
```
Always include timeout signals for network resilience. Transform data into appropriate formats when needed. Multiple `nostr.query()` calls can be made in a single queryFn.
Use the `useAuthor` hook to fetch profile metadata by pubkey:
```tsx
import { NostrEvent } from '@nostrify/nostrify';
import { useAuthor } from '@/hooks/useAuthor';
function Post({ event }: { event: NostrEvent }) {
const author = useAuthor(event.pubkey);
const metadata = author.data?.metadata;
const displayName = metadata?.name || event.pubkey.slice(0, 8);
const profileImage = metadata?.picture;
// render with this data
}
```
**NostrMetadata** fields: `about`, `banner`, `bot`, `display_name`, `lud06`, `lud16`, `name`, `nip05`, `picture`, `website`
Use `useNostrPublish` with `useCurrentUser` to ensure authentication:
```tsx
import { useState } from 'react';
import { useCurrentUser } from "@/hooks/useCurrentUser";
import { useNostrPublish } from '@/hooks/useNostrPublish';
export function MyComponent() {
const [data, setData] = useState<Record<string, string>>({});
const { user } = useCurrentUser();
const { mutate: createEvent } = useNostrPublish();
const handleSubmit = () => {
createEvent({ kind: 1, content: data.content });
};
if (!user) {
return <span>You must be logged in to use this form.</span>;
}
return (
<form onSubmit={handleSubmit} disabled={!user}>
{/* input fields */}
</form>
);
}
```
Always guard publish actions with user authentication checks.
Use the `LoginArea` component for Nostr login/logout:
```tsx
import { LoginArea } from "@/components/auth/LoginArea";
function MyComponent() {
return (
<div>
<LoginArea />
</div>
);
}
```
The `LoginArea` handles all login UI, account switching, and dialogs automatically. Do not wrap it in conditional logic.
Nostr uses special identifiers with prefixes:
**IMPORTANT**: Filters require hex strings, not NIP-19 identifiers. Always decode first:
```ts
import { nip19 } from 'nostr-tools';
const decoded = nip19.decode(value);
if (decoded.type !== 'naddr') {
throw new Error('Unsupported Nostr identifier');
}
const naddr = decoded.data;
const events = await nostr.query(
[{
kinds: [naddr.kind],
authors: [naddr.pubkey],
'#d': [naddr.identifier],
}],
{ signal }
);
```
Include the `EditProfileForm` component for profile management:
```tsx
import { EditProfileForm } from "@/components/EditProfileForm";
function EditProfilePage() {
return <EditProfileForm />;
}
```
The component requires no props and handles all profile editing automatically.
Use `useUploadFile` for file uploads with NIP-94 compatibility:
```tsx
import { useUploadFile } from "@/hooks/useUploadFile";
function MyComponent() {
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const handleUpload = async (file: File) => {
try {
const [[_, url]] = await uploadFile(file);
// use the url
} catch (error) {
// handle errors
}
};
}
```
For kind 1 events: append URLs to `content` and add `imeta` tags for each file.
For kind 0 events: use URLs directly in JSON content fields.
Use the logged-in user's signer for NIP-44 encryption:
```ts
const { user } = useCurrentUser();
if (!user.signer.nip44) {
throw new Error("Please upgrade your signer extension to support NIP-44");
}
// Encrypt to self
const encrypted = await user.signer.nip44.encrypt(user.pubkey, "hello world");
// Decrypt from self
const decrypted = await user.signer.nip44.decrypt(user.pubkey, encrypted);
```
Always check for NIP-44 availability before using encryption features.
1. **Component Architecture**: Use React hooks and functional components exclusively
2. **State Management**: Use TanStack Query for server state, React hooks for UI state
3. **Styling**: Use TailwindCSS utility classes; leverage shadcn/ui component patterns
4. **Type Safety**: Always use TypeScript with proper type annotations
5. **Imports**: Use `@/` path alias for all internal imports
6. **Network Resilience**: Include timeout signals in all Nostr queries
7. **Authentication**: Guard all publish/sensitive actions with user checks
**CRITICAL**: Always test changes by running:
```bash
npm run ci
```
This command typechecks and builds the project. Your task is not complete until this passes without errors.
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/nostr-client-development-with-react/raw