2025-10-23 12:16:25 +05:30
## OpChan React SDK (packages/react) — Building Forum UIs
This guide shows how to build a forums-based app using the React SDK. It covers project wiring, wallet connection/disconnection, key delegation, waiting for Waku/network readiness, loading content, posting/commenting, voting, moderation, bookmarks, and user display utilities.
The examples assume you install and use the `@opchan/react` and `@opchan/core` packages.
---
### 1) Install and basic setup
```bash
npm i @opchan/react @opchan/core
```
2025-10-28 12:45:05 +05:30
Create an app-level provider using `OpChanProvider` . The provider already wraps `WagmiProvider` and React Query; no AppKit is required. Mount it directly at the app root.
2025-10-23 12:16:25 +05:30
```tsx
import React from 'react';
import { OpChanProvider } from '@opchan/react ';
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
2025-10-28 12:45:05 +05:30
< OpChanProvider config = {{}} >
2025-10-28 11:29:21 +05:30
{children}
< / OpChanProvider >
2025-10-23 12:16:25 +05:30
);
}
```
2025-10-28 12:45:05 +05:30
OpChanProvider uses wagmi connectors (Injected/WalletConnect/Coinbase) for wallet management.
2025-10-23 12:16:25 +05:30
---
### 2) High-level hook
Use `useForum()` for a single entry point to the main hooks:
```tsx
import { useForum } from '@opchan/react ';
function Example() {
const { user, content, permissions, network } = useForum();
// user: auth + delegation
// content: cells/posts/comments/bookmarks + actions
// permissions: derived booleans + reasons
// network: Waku readiness + refresh
return null;
}
```
---
### 3) Wallets — connect/disconnect & verification
API: `useAuth()`
```tsx
import { useAuth } from '@opchan/react ';
function WalletControls() {
const { currentUser, verificationStatus, connect, disconnect, verifyOwnership } = useAuth();
return (
< div >
{currentUser ? (
< >
< div > Connected: {currentUser.displayName}< / div >
< button onClick = {() = > disconnect()}>Disconnect< / button >
2025-10-28 12:45:05 +05:30
< button onClick = {() = > verifyOwnership()}>Verify ENS< / button >
2025-10-23 12:16:25 +05:30
< div > Status: {verificationStatus}< / div >
< />
) : (
< >
2025-10-28 12:45:05 +05:30
< button onClick = {() = > connect()}>Connect Wallet< / button >
2025-10-23 12:16:25 +05:30
< />
)}
< / div >
);
}
```
Notes:
2025-10-28 12:45:05 +05:30
- `connect()` opens the selected wagmi connector (e.g., Injected/WalletConnect). Upon successful connection, OpChan automatically syncs the wallet state and creates a user session.
- `verifyOwnership()` refreshes identity and sets `EVerificationStatus` appropriately (checks ENS).
2025-10-23 12:16:25 +05:30
---
### 4) Key delegation — create, check, clear
API: `useAuth()` → `delegate(duration)` , `delegationStatus()` , `clearDelegation()` ; also `delegationInfo` in-session.
```tsx
import { useAuth } from '@opchan/react ';
function DelegationControls() {
const { delegate, delegationInfo, delegationStatus, clearDelegation } = useAuth();
return (
< div >
< div >
Delegated: {String(delegationInfo.isValid)}
{delegationInfo.expiresAt && `, expires ${delegationInfo.expiresAt.toLocaleString()}` }
< / div >
< button onClick = {() = > delegate('7days')}>Delegate 7 days< / button >
< button onClick = {() = > delegate('30days')}>Delegate 30 days< / button >
< button onClick = {() = > delegationStatus().then(console.log)}>Check< / button >
< button onClick = {() = > clearDelegation()}>Clear< / button >
< / div >
);
}
```
Behavior:
- The library generates a browser keypair and requests a wallet signature authorizing it until a selected expiry.
- All messages (cells/posts/comments/votes/moderation/profile updates) are signed with the delegated browser key and verified via the proof.
---
### 5) Network (Waku) — readiness and manual refresh
API: `useNetwork()`
```tsx
import { useNetwork } from '@opchan/react ';
function NetworkStatus() {
const { isConnected, statusMessage, refresh } = useNetwork();
return (
< div >
< span > {isConnected ? 'Connected' : 'Connecting...'}< / span >
< span > • {statusMessage}< / span >
< button onClick = {() = > refresh()}>Refresh< / button >
< / div >
);
}
```
Notes:
- The store wires Waku health events to `network.isConnected` and `statusMessage` .
- `refresh()` triggers a lightweight cache refresh; live updates come from the Waku subscription.
---
### 6) Loading content — cells, posts, comments, bookmarks
API: `useContent()`
```tsx
import { useContent } from '@opchan/react ';
function Feed() {
const { cells, posts, comments, postsByCell, commentsByPost, bookmarks, lastSync } = useContent();
return (
< div >
< div > Cells: {cells.length}< / div >
< div > Posts: {posts.length}< / div >
< div > Comments: {comments.length}< / div >
< div > Bookmarks: {bookmarks.length}< / div >
< div > Last sync: {lastSync ? new Date(lastSync).toLocaleTimeString() : '—'}< / div >
< / div >
);
}
```
Derived helpers:
- `postsByCell[cellId]: Post[]`
- `commentsByPost[postId]: Comment[]` (sorted oldest→newest)
- `cellsWithStats` : adds `postCount` , `activeUsers` , `recentActivity` for UI
- `userVerificationStatus[address]` : identity verification snapshot for weighting/relevance
- `pending.isPending(id)` : show optimistic “syncing…” indicators
---
### 7) Creating content — cells, posts, comments
All actions reflect to the local cache immediately and then propagate over the network.
```tsx
import { useContent } from '@opchan/react ';
function Composer({ cellId, postId }: { cellId?: string; postId?: string }) {
const { createCell, createPost, createComment } = useContent();
const onCreateCell = async () => {
await createCell({ name: 'My Cell', description: 'Description', icon: '' });
};
const onCreatePost = async () => {
if (!cellId) return;
await createPost({ cellId, title: 'Hello', content: 'World' });
};
const onCreateComment = async () => {
if (!postId) return;
await createComment({ postId, content: 'Nice post!' });
};
return (
< div >
< button onClick = {onCreateCell} > Create Cell< / button >
< button onClick = {onCreatePost} disabled = {!cellId} > Create Post< / button >
< button onClick = {onCreateComment} disabled = {!postId} > Create Comment< / button >
< / div >
);
}
```
Permissions: see `usePermissions()` below to gate UI.
---
### 8) Voting
```tsx
import { useContent } from '@opchan/react ';
function VoteButtons({ targetId }: { targetId: string }) {
const { vote, pending } = useContent();
const isSyncing = pending.isPending(targetId);
return (
< div >
< button onClick = {() = > vote({ targetId, isUpvote: true })}>Upvote< / button >
< button onClick = {() = > vote({ targetId, isUpvote: false })}>Downvote< / button >
{isSyncing & & < span > syncing…< / span > }
< / div >
);
}
```
---
### 9) Moderation (cell owner)
```tsx
import { useContent, usePermissions } from '@opchan/react ';
function Moderation({ cellId, postId, commentId, userAddress }: { cellId: string; postId?: string; commentId?: string; userAddress?: string }) {
const { moderate } = useContent();
const { canModerate } = usePermissions();
if (!canModerate(cellId)) return null;
return (
< div >
{postId & & (
< >
< button onClick = {() = > moderate.post(cellId, postId, 'reason')}>Moderate post< / button >
< button onClick = {() = > moderate.unpost(cellId, postId, 'reason')}>Unmoderate post< / button >
< />
)}
{commentId & & (
< >
< button onClick = {() = > moderate.comment(cellId, commentId, 'reason')}>Moderate comment< / button >
< button onClick = {() = > moderate.uncomment(cellId, commentId, 'reason')}>Unmoderate comment< / button >
< />
)}
{userAddress & & (
< >
< button onClick = {() = > moderate.user(cellId, userAddress, 'reason')}>Moderate user< / button >
< button onClick = {() = > moderate.unuser(cellId, userAddress, 'reason')}>Unmoderate user< / button >
< />
)}
< / div >
);
}
```
---
### 10) Bookmarks
```tsx
import { useContent } from '@opchan/react ';
function BookmarkControls({ post, comment }: { post?: any; comment?: any }) {
const { bookmarks, togglePostBookmark, toggleCommentBookmark, removeBookmark, clearAllBookmarks } = useContent();
return (
< div >
< div > Total: {bookmarks.length}< / div >
{post & & < button onClick = {() = > togglePostBookmark(post, post.cellId)}>Toggle post bookmark< / button > }
{comment & & < button onClick = {() = > toggleCommentBookmark(comment, comment.postId)}>Toggle comment bookmark< / button > }
< button onClick = {() = > bookmarks[0] & & removeBookmark(bookmarks[0].id)}>Remove first< / button >
< button onClick = {() = > clearAllBookmarks()}>Clear all< / button >
< / div >
);
}
```
---
### 11) Permissions helper
API: `usePermissions()` exposes booleans and user-friendly reasons for gating UI.
```tsx
import { usePermissions } from '@opchan/react ';
function ActionGates({ cellId }: { cellId: string }) {
const p = usePermissions();
return (
< ul >
< li > Can create cell: {String(p.canCreateCell)} ({p.reasons.createCell})< / li >
< li > Can post: {String(p.canPost)} ({p.reasons.post})< / li >
< li > Can comment: {String(p.canComment)} ({p.reasons.comment})< / li >
< li > Can vote: {String(p.canVote)} ({p.reasons.vote})< / li >
< li > Can moderate: {String(p.canModerate(cellId))} ({p.reasons.moderate(cellId)})< / li >
< / ul >
);
}
```
---
### 12) User display/identity helpers
API: `useUserDisplay(address)` returns resolved user identity for UI labels.
```tsx
import { useUserDisplay } from '@opchan/react ';
function AuthorName({ address }: { address: string }) {
2025-10-28 12:45:05 +05:30
const { displayName, ensName, isLoading } = useUserDisplay(address);
2025-10-23 12:16:25 +05:30
if (isLoading) return < span > Loading…< / span > ;
2025-10-28 12:45:05 +05:30
return < span title = {ensName | | undefined } > {displayName}< / span > ;
2025-10-23 12:16:25 +05:30
}
```
---
### 13) End-to-end page skeleton
```tsx
import React from 'react';
import { OpChanProvider, useForum } from '@opchan/react ';
function Home() {
const { user, content, permissions, network } = useForum();
return (
< div >
< div > Network: {network.isConnected ? 'Ready' : 'Connecting…'}< / div >
{user.currentUser ? (
< div > Welcome {user.currentUser.displayName}< / div >
) : (
< button onClick = {() = > user.connect({ address: '0xabc...', walletType: 'ethereum' })}>Connect< / button >
)}
{permissions.canPost & & content.cells[0] & & (
< button onClick = {() = > content.createPost({ cellId: content.cells[0].id, title: 'Hi', content: 'Hello world' })}>
New Post
< / button >
)}
< ul >
{content.posts.map(p => (
< li key = {p.id} > {p.title}< / li >
))}
< / ul >
< / div >
);
}
export default function App() {
return (
2025-10-28 12:45:05 +05:30
< OpChanProvider config = {{}} >
2025-10-23 12:16:25 +05:30
< Home / >
< / OpChanProvider >
);
}
```
---
### 14) Notes and best practices
- Always gate actions with `usePermissions()` to provide clear UX reasons.
- Use `pending.isPending(id)` to show optimistic “syncing…” states for content you just created or voted on.
- The store is hydrated from IndexedDB on load; Waku live messages keep it in sync.
2025-10-28 12:45:05 +05:30
- Identity (ENS/Call Sign) is resolved and cached; calling `verifyOwnership()` or updating the profile will refresh it.
2025-10-23 12:16:25 +05:30