EmProps React Web3 - Pattern Reference
Reference Only
This documentation covers patterns from the emprops-react-web3 demo package. Do not migrate this package - instead, use it as a reference for frontend patterns when building NFT UI components.
Purpose: Extract useful patterns (adapters, hooks, types) Approach: Reference and learn, don't copy wholesale
Related Documentation
- Arbitrum Overview - The larger system context
- Technical Architecture - How frontend connects to blockchain
- Quick Start - Simplified patterns for development
Version: 0.0.1 Package Name: @emprops/web3Status: Reference only (not for migration) Last Updated: 2025-11-09
Table of Contents
- Overview
- Purpose & Role
- Architecture
- Key Patterns to Extract
- Provider System
- Adapter Pattern
- Hook Architecture
- Type System
- Environment Management
- Testing Approach
- Integration Recommendations
- What NOT to Copy
Overview
What Is This Package?
emprops-react-web3 is a demonstration/testing SDK built 8 months ago to validate the integration between:
- Smart contracts (hardhat)
- Blockchain indexing (ponder)
- React frontend
Key Point: This is NOT production code to be migrated wholesale. It's a proof-of-concept that demonstrates useful patterns.
Package Structure
emprops-react-web3/
├── src/
│ ├── adapter/ # 🌟 Dual data source pattern
│ ├── clients/ # API client setup
│ ├── hooks/ # React hooks for Web3 operations
│ ├── provider/ # Context provider setup
│ ├── types/ # TypeScript types
│ ├── utils/ # Utility functions
│ ├── env.ts # Environment validation (Zod)
│ └── index.ts # Main exports
│
├── test/manual/ # Manual testing components
├── vitest.config.ts # Test configuration
├── tsup.config.ts # Build configuration
└── package.jsonTechnology Stack
{
"react": "^18.3.1",
"wagmi": "^2.0.0", // Ethereum React hooks
"viem": "^2.0.0", // Ethereum library
"@rainbow-me/rainbowkit": "^2.0.0", // Wallet UI
"@tanstack/react-query": "^5.0.0", // State management
"socket.io-client": "^4.8.1", // Real-time updates
"zod": "^3.24.2" // Schema validation
}Purpose & Role
Original Intent
This package was created to:
- ✅ Test the full stack (contracts → ponder → frontend)
- ✅ Validate data flow patterns
- ✅ Demonstrate dual data sources (on-chain vs indexed)
- ✅ Provide manual testing components
NOT Intended For
- ❌ Production use as a published package
- ❌ Wholesale migration into monorepo
- ❌ Direct copy-paste into nft-launchpad
Correct Usage in Migration
DO:
- ✅ Study the adapter pattern
- ✅ Learn from hook structure
- ✅ Extract type definitions
- ✅ Understand React Query integration
- ✅ Reference environment validation approach
DON'T:
- ❌ Copy the entire provider system
- ❌ Create another "@emprops/web3" package
- ❌ Duplicate Wagmi/RainbowKit setup
- ❌ Build abstractions over abstractions
Architecture
High-Level Architecture
┌──────────────────────────────────────────────────────┐
│ EmPropsProvider │
│ (React Context) │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Wagmi Config │ │
│ │ - Wallet connection │ │
│ │ - Network config │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ React Query Client │ │
│ │ - Query caching │ │
│ │ - Mutation handling │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Socket.io Client │ │
│ │ - Real-time event subscriptions │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
│
┌─────────────────┴─────────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ WagmiAdapter │ │ PonderAdapter │
│ │ │ │
│ - On-chain reads │ │ - API calls │
│ - Direct RPC │ │ - Indexed data │
│ - Latest state │ │ - Historical │
└──────────────────┘ └──────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Blockchain │ │ Ponder API │
│ (via RPC) │ │ (PostgreSQL) │
└──────────────────┘ └──────────────────┘Data Flow
1. Component calls hook (e.g., useGetSimpleApps)
│
├─> Hook decides: use Wagmi or Ponder adapter?
│
├─> WagmiAdapter: Direct blockchain read
│ └─> Via Wagmi's useReadContract
│
└─> PonderAdapter: API call to indexer
└─> fetch('http://localhost:42069/api/apps')Key Patterns to Extract
1. Adapter Pattern (🌟 Most Valuable)
Purpose: Abstract data source (on-chain vs indexed)
Pattern:
// Define adapter interface
export interface DataAdapter {
readContract<T>(params: ReadContractParams): Promise<T>;
queryData<T>(endpoint: string): Promise<T>;
}
// Wagmi implementation
export class WagmiAdapter implements DataAdapter {
async readContract<T>(params: ReadContractParams): Promise<T> {
const client = createPublicClient({ /* ... */ });
return await client.readContract(params);
}
}
// Ponder implementation
export class PonderAdapter implements DataAdapter {
async queryData<T>(endpoint: string): Promise<T> {
const response = await fetch(`${PONDER_URL}${endpoint}`);
return await response.json();
}
}
// Usage in hooks
const adapter = usePonderAdapter(); // or useWagmiAdapter()
const data = await adapter.queryData('/api/apps');Why This Matters:
- ✅ Flexibility: Switch between on-chain and indexed data
- ✅ Testing: Mock adapters easily
- ✅ Performance: Use indexed data for lists, on-chain for critical reads
Implementation in nft-launchpad:
// apps/nft-launchpad/lib/adapters/
// Don't recreate the adapter classes verbatim
// Just use the pattern concept:
// For lists → fetch from API
export async function getCollections() {
return fetch(`${NFT_INDEXER_URL}/api/apps`);
}
// For critical reads → use Wagmi directly
export function useCollectionMaxSupply(address: string) {
return useReadContract({
address,
abi: SimpleAppAbi,
functionName: 'maxSupply'
});
}2. React Query Integration
Pattern: Combine Wagmi with React Query for caching
From src/hooks/useEmPropsQuery.ts:
export function useEmPropsQuery<TData>(options: {
contractName: string;
functionName: string;
args?: any[];
enabled?: boolean;
select?: (data: any) => TData;
watchEvents?: string[]; // Socket.io events that trigger refetch
}) {
const queryClient = useQueryClient();
// Subscribe to WebSocket events
useEffect(() => {
if (!options.watchEvents) return;
const socket = io(PONDER_URL);
options.watchEvents.forEach(event => {
socket.on(event, () => {
queryClient.invalidateQueries({ queryKey: [options.contractName, options.functionName] });
});
});
return () => socket.disconnect();
}, [options.watchEvents]);
// Use React Query
return useQuery({
queryKey: [options.contractName, options.functionName, options.args],
queryFn: async () => {
// Fetch data from adapter
},
select: options.select,
enabled: options.enabled
});
}Key Insight: WebSocket events trigger React Query cache invalidation
Implementation in nft-launchpad:
// apps/nft-launchpad/hooks/useCollections.ts
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { io } from 'socket.io-client';
export function useCollections() {
const queryClient = useQueryClient();
// Fetch collections
const query = useQuery({
queryKey: ['collections'],
queryFn: async () => {
const response = await fetch(`${NFT_INDEXER_URL}/api/apps`);
return response.json();
}
});
// Listen for real-time updates
useEffect(() => {
const socket = io(NFT_INDEXER_URL);
socket.on('emprops:app:created', () => {
queryClient.invalidateQueries({ queryKey: ['collections'] });
});
return () => socket.disconnect();
}, [queryClient]);
return query;
}3. Transaction Hook Pattern
Pattern: Standardized transaction submission with Wagmi
From src/hooks/useEmPropsTransaction.ts:
export function useEmPropsTransaction(options: {
contractName: string;
functionName: string;
address: (params: any) => Address;
prepare?: (params: any, helpers: any) => Promise<{ args: any[]; value?: bigint }>;
}) {
const { writeContractAsync } = useWriteContract();
const txSubmit = async (params: any) => {
// 1. Prepare transaction (get args, value)
const { args, value } = await options.prepare?.(params, helpers) ?? { args: [] };
// 2. Submit transaction
const hash = await writeContractAsync({
address: options.address(params),
abi: contractAbi,
functionName: options.functionName,
args,
value
});
// 3. Wait for confirmation
await waitForTransactionReceipt({ hash });
return { hash };
};
return { txSubmit, isMining };
}Why This Matters:
- ✅ Consistent transaction pattern
- ✅ Built-in validation (prepare step)
- ✅ Loading states handled
Implementation in nft-launchpad:
// apps/nft-launchpad/hooks/useMintNFT.ts
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
export function useMintNFT(collectionAddress: `0x${string}`) {
const { writeContractAsync } = useWriteContract();
const [hash, setHash] = useState<`0x${string}`>();
const { isLoading: isConfirming } = useWaitForTransactionReceipt({ hash });
const mint = async (quantity: number) => {
// Get mint price from contract
const mintPrice = await readContract({
address: collectionAddress,
abi: SimpleAppAbi,
functionName: 'mintPrice'
});
// Submit transaction
const txHash = await writeContractAsync({
address: collectionAddress,
abi: SimpleAppAbi,
functionName: 'mint',
args: [BigInt(quantity)],
value: mintPrice * BigInt(quantity)
});
setHash(txHash);
return txHash;
};
return { mint, isConfirming };
}4. Type System Structure
Pattern: Organized type definitions
From src/types/:
types/
├── contracts.ts # Contract interfaces & types
├── emprops.ts # Domain types (EmPropsApp, etc.)
├── queries.ts # Query parameter types
├── transactions.ts # Transaction parameter types
└── env.d.ts # Environment variable typesExample - Contract Types:
// types/contracts.ts
export interface CreateAppParams {
name: string;
symbol: string;
maxSupply: bigint;
mintPrice: bigint;
maxPerMint?: bigint;
startInDays?: number;
}
export interface NFTContractFactoryContract {
createNFTContract: (params: CreateAppParams) => Promise<{ hash: string; appAddress: string }>;
}Implementation in nft-launchpad:
// apps/nft-launchpad/types/collection.ts
export interface CreateCollectionParams {
name: string;
symbol: string;
maxSupply: number;
mintPrice: string; // In ETH
maxPerMint: number;
startDate: Date;
}
export interface Collection {
address: string;
name: string;
symbol: string;
maxSupply: string;
mintPrice: string;
totalMinted: string;
ownerTokenId: string;
}5. Environment Validation with Zod
Pattern: Validate env vars at runtime
From src/env.ts:
import { z } from 'zod';
const envSchema = z.object({
NEXT_PUBLIC_PONDER_API_URL: z.string().url(),
NEXT_PUBLIC_CHAIN_ID: z.coerce.number(),
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: z.string().optional()
});
export const env = envSchema.parse(process.env);Why This Matters:
- ✅ Catch missing env vars early
- ✅ Type-safe environment access
- ✅ Clear error messages
Implementation in nft-launchpad:
// apps/nft-launchpad/lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
NEXT_PUBLIC_NFT_INDEXER_URL: z.string().url(),
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: z.string().min(1)
});
export const env = envSchema.parse({
NEXT_PUBLIC_NFT_INDEXER_URL: process.env.NEXT_PUBLIC_NFT_INDEXER_URL,
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
});Provider System
How It Works
File: src/provider/EmPropsProvider.tsx
export function EmPropsProvider({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
<EmPropsContext.Provider value={contextValue}>
{children}
</EmPropsContext.Provider>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}Context Value:
const contextValue = {
contracts: {
NFTContractFactory: { address, abi },
OwnerToken: { address, abi },
SimpleApp: { abi } // No address - each instance is different
},
adapter: ponderAdapter,
isConnected: Boolean(address)
};Why NOT to Copy This
Problems with this approach:
- ❌ Over-abstraction - wraps Wagmi which already works well
- ❌ Extra layer of complexity - provider within provider within provider
- ❌ Contract info should come from API, not context
- ❌ Harder to debug than direct Wagmi usage
Better Approach for nft-launchpad
Just use Wagmi and RainbowKit directly:
// apps/nft-launchpad/pages/_app.tsx
import { WagmiProvider, createConfig, http } from 'wagmi';
import { baseSepolia } from 'wagmi/chains';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit';
const config = getDefaultConfig({
appName: 'NFT Launchpad',
projectId: env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID,
chains: [baseSepolia],
transports: {
[baseSepolia.id]: http()
}
});
const queryClient = new QueryClient();
export default function App({ Component, pageProps }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
<Component {...pageProps} />
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}No custom provider needed! Wagmi already provides everything via hooks.
Adapter Pattern
Files
src/adapter/WagmiAdapter.ts- On-chain readssrc/adapter/PonderAdapter.ts- API readssrc/adapter/types.ts- Shared interfaces
WagmiAdapter
Purpose: Read contract data directly from blockchain
export class WagmiAdapter {
constructor(private publicClient: PublicClient) {}
async readContract<T>({ address, abi, functionName, args }: ReadParams): Promise<T> {
return await this.publicClient.readContract({
address,
abi,
functionName,
args
});
}
}Use Cases:
- Reading current mint price (needs to be up-to-date)
- Checking if minting is paused
- Getting collection owner
PonderAdapter
Purpose: Read indexed data from Ponder API
export class PonderAdapter {
constructor(private apiUrl: string) {}
async queryData<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.apiUrl}${endpoint}`);
if (!response.ok) throw new Error('API request failed');
return await response.json();
}
}Use Cases:
- Listing all collections (indexed data)
- Getting mint history (historical events)
- Viewing token ownership over time
Adapter Selection Logic
From hooks:
// For lists and historical data → Ponder
const collections = await ponderAdapter.queryData('/api/apps');
// For real-time critical reads → Wagmi
const isPaused = await wagmiAdapter.readContract({
address: collectionAddress,
functionName: 'paused'
});Simplified Version for nft-launchpad
Don't create adapter classes. Just make the distinction in function naming:
// apps/nft-launchpad/lib/api.ts (uses Ponder)
export async function fetchCollections() {
const response = await fetch(`${NFT_INDEXER_URL}/api/apps`);
return response.json();
}
export async function fetchCollection(address: string) {
const response = await fetch(`${NFT_INDEXER_URL}/api/apps/${address}`);
return response.json();
}
// apps/nft-launchpad/lib/contracts.ts (uses Wagmi)
export function useCollectionOnChain(address: string) {
const { data: paused } = useReadContract({
address,
abi: SimpleAppAbi,
functionName: 'paused'
});
const { data: mintPrice } = useReadContract({
address,
abi: SimpleAppAbi,
functionName: 'mintPrice'
});
return { paused, mintPrice };
}Clearer and more direct!
Hook Architecture
Query Hooks
Location: src/hooks/queries/
Example: useGetSimpleApps
export function useGetSimpleApps(options?: GetSimpleAppsOptions) {
const watchEvents = useMemo(() => {
if (options?.hydration) return undefined;
return [
'emprops:app:created',
'emprops:appToken:minted',
'emprops:contract:upgraded',
];
}, [options?.additionalWatches]);
return useEmPropsQuery<SimpleApp[]>({
contractName: 'NFTContractFactoryContract',
functionName: 'apps',
args: [],
enabled: options?.enabled,
select: (data) => {
// Transform raw data to app objects
return data.map(app => ({
id: app.id,
name: app.name,
address: app.id,
// ... more fields
}));
},
watchEvents
});
}Key Features:
- ✅ Real-time updates via
watchEvents - ✅ Data transformation via
select - ✅ Optional hydration mode (SSR)
Transaction Hooks
Location: src/hooks/transactions/
Example: useMintToken
export function useMintToken() {
const transaction = useEmPropsTransaction({
contractName: 'SimpleAppContract',
functionName: 'mint',
address: (params: MintTokenParams) => params.appAddress as Address,
prepare: async (params, { readContract }) => {
// Validate before transaction
const isPaused = await readContract({
contractName: 'SimpleAppContract',
functionName: 'paused',
address: params.appAddress
});
if (isPaused) throw new Error('Minting is paused');
// Get mint price
const mintPrice = await readContract({
contractName: 'SimpleAppContract',
functionName: 'mintPrice',
address: params.appAddress
});
return {
args: [params.quantity],
value: mintPrice * params.quantity
};
}
});
return {
txSubmit: transaction.txSubmit,
isMining: transaction.isMining
};
}Key Features:
- ✅ Pre-transaction validation
- ✅ Automatic value calculation
- ✅ Loading state management
Simplified Approach for nft-launchpad
Don't wrap Wagmi hooks. Use them directly:
// apps/nft-launchpad/hooks/useMintNFT.ts
export function useMintNFT(collectionAddress: `0x${string}`) {
const { data: mintPrice } = useReadContract({
address: collectionAddress,
abi: SimpleAppAbi,
functionName: 'mintPrice'
});
const { data: isPaused } = useReadContract({
address: collectionAddress,
abi: SimpleAppAbi,
functionName: 'paused'
});
const { writeContractAsync, isPending } = useWriteContract();
const mint = async (quantity: number) => {
if (isPaused) throw new Error('Minting is paused');
if (!mintPrice) throw new Error('Mint price not loaded');
return await writeContractAsync({
address: collectionAddress,
abi: SimpleAppAbi,
functionName: 'mint',
args: [BigInt(quantity)],
value: mintPrice * BigInt(quantity)
});
};
return { mint, isPending, isPaused, mintPrice };
}More explicit, easier to understand, same functionality.
Type System
Contract Types
File: src/types/contracts.ts
Defines contract interfaces:
export interface BaseContract {
address: string;
abi: any[];
}
export interface CreateAppParams {
name: string;
symbol: string;
maxSupply: bigint;
mintPrice: bigint;
maxPerMint?: bigint;
startInDays?: number;
}For nft-launchpad:
- ✅ Copy type definitions
- ✅ Adjust to match your needs
- ✅ Keep types in
types/directory
Domain Types
File: src/types/emprops.ts
export interface EmPropsApp {
id: string;
type: string;
name: string;
symbol: string;
address: string;
ownerTokenId: string;
maxSupply: string;
mintPrice: string;
totalMinted: string;
}For nft-launchpad:
// apps/nft-launchpad/types/collection.ts
export interface Collection {
address: string;
name: string;
symbol: string;
type: string;
ownerTokenId: string;
maxSupply: string;
mintPrice: string;
maxPerMint: string;
totalMinted: string;
paused: boolean;
startDateTime: string;
}Environment Management
Zod Validation
From src/env.ts:
import { z } from 'zod';
const envSchema = z.object({
NEXT_PUBLIC_PONDER_API_URL: z.string().url(),
NEXT_PUBLIC_CHAIN_ID: z.coerce.number(),
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: z.string().optional()
});
export const env = envSchema.parse(process.env);Benefits:
- Validates at build/runtime
- Type-safe access:
env.NEXT_PUBLIC_PONDER_API_URL - Clear error messages if missing
For nft-launchpad:
// apps/nft-launchpad/lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
NEXT_PUBLIC_NFT_INDEXER_URL: z.string().url(),
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: z.string().min(1),
NEXT_PUBLIC_CHAIN_ID: z.coerce.number().default(84532) // Base Sepolia
});
export const env = envSchema.parse({
NEXT_PUBLIC_NFT_INDEXER_URL: process.env.NEXT_PUBLIC_NFT_INDEXER_URL,
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID,
NEXT_PUBLIC_CHAIN_ID: process.env.NEXT_PUBLIC_CHAIN_ID
});
// Usage:
// import { env } from '@/lib/env';
// fetch(`${env.NEXT_PUBLIC_NFT_INDEXER_URL}/api/apps`);Testing Approach
Manual Testing Components
Location: src/test/manual/
The package includes manual testing components to validate functionality:
// src/test/manual/TestMint.tsx
export function TestMint() {
const { mint, isPending } = useMintToken();
const handleMint = async () => {
await mint({
appAddress: '0x...',
quantity: 1n
});
};
return (
<button onClick={handleMint} disabled={isPending}>
{isPending ? 'Minting...' : 'Mint NFT'}
</button>
);
}Why This Matters:
- ✅ Manual testing is valuable for Web3
- ✅ Can test on testnet with real transactions
- ✅ Helps validate UX before building full UI
For nft-launchpad:
- Create similar test pages
- Use them to validate contract interactions
- Keep them in
pages/test/during development - Remove before production
Unit Testing
Framework: Vitest Config: vitest.config.ts
export default defineConfig({
test: {
environment: 'jsdom',
coverage: {
provider: 'v8',
reporter: ['text', 'html']
}
}
});Tests:
- Hook behavior
- Type transformations
- Error handling
Integration Recommendations
What to Extract
1. Adapter Pattern Concept ⭐⭐⭐⭐⭐
Copy: The idea, not the code Why: Flexibility between on-chain and indexed data How: Direct API calls vs Wagmi hooks
2. React Query + WebSocket Pattern ⭐⭐⭐⭐⭐
Copy: Exact pattern Why: Real-time updates with caching How: Socket.io listeners trigger invalidateQueries
3. Type Definitions ⭐⭐⭐⭐
Copy: Type structure and organization Why: Type safety and maintainability How: Copy types, adjust for your needs
4. Environment Validation ⭐⭐⭐⭐
Copy: Zod validation approach Why: Catch config errors early How: Create lib/env.ts with schema
5. Transaction Preparation Pattern ⭐⭐⭐
Copy: Pre-transaction validation concept Why: Better UX and fewer failed transactions How: Validate before calling writeContract
What NOT to Extract
1. Custom Provider System ❌
Don't Copy: EmPropsProvider wrapper Why: Adds unnecessary abstraction Alternative: Use Wagmi/RainbowKit directly
2. Adapter Classes ❌
Don't Copy: WagmiAdapter/PonderAdapter classes Why: Over-engineered for simple use case Alternative: Direct API calls and Wagmi hooks
3. useEmPropsQuery Wrapper ❌
Don't Copy: Custom query hook wrapper Why: Adds complexity without much benefit Alternative: Use useQuery directly with Socket.io
4. Contract Registry in Context ❌
Don't Copy: Storing contracts in React Context Why: Should come from API, not hardcoded Alternative: Fetch from NFT indexer API
What NOT to Copy
Anti-Patterns to Avoid
Over-Abstraction
- ❌ Don't wrap Wagmi hooks with custom hooks
- ✅ Use Wagmi hooks directly
Provider Inception
- ❌ Don't create provider wrapping providers
- ✅ Use Wagmi/RainbowKit providers directly
Hardcoded Contract Info
- ❌ Don't store ABIs in context
- ✅ Fetch from API or import from package
Generic Hook Wrappers
- ❌ Don't create
useEmPropsQuerywrapper - ✅ Create specific hooks like
useCollections
- ❌ Don't create
Adapter Classes for Simple Cases
- ❌ Don't create adapter classes
- ✅ Use functions:
fetchFromAPI()anduseReadContract()
Conclusion
Summary
emprops-react-web3 is a valuable reference implementation that demonstrates:
✅ Dual Data Sources - On-chain vs indexed ✅ Real-Time Updates - Socket.io + React Query ✅ Type Safety - TypeScript throughout ✅ Environment Validation - Zod schemas ✅ Transaction Patterns - Pre-validation and preparation
Migration Approach
DO:
- ✅ Study and understand patterns
- ✅ Extract type definitions
- ✅ Copy React Query + Socket.io pattern
- ✅ Use environment validation approach
- ✅ Reference for testing strategies
DON'T:
- ❌ Migrate as a package
- ❌ Copy provider system
- ❌ Copy adapter classes
- ❌ Copy hook wrappers
- ❌ Add abstraction layers
For nft-launchpad
Build fresh with lessons learned:
// Simple, direct, maintainable:
// API calls
const collections = await fetch(`${NFT_INDEXER_URL}/api/apps`);
// On-chain reads
const { data } = useReadContract({
address: collectionAddress,
abi: SimpleAppAbi,
functionName: 'mintPrice'
});
// Real-time updates
useEffect(() => {
const socket = io(NFT_INDEXER_URL);
socket.on('emprops:app:created', () => {
queryClient.invalidateQueries(['collections']);
});
return () => socket.disconnect();
}, []);
// Transactions
const { writeContractAsync } = useWriteContract();
await writeContractAsync({
address: collectionAddress,
abi: SimpleAppAbi,
functionName: 'mint',
args: [quantity],
value: mintPrice * quantity
});That's it! No custom providers, no adapters, no wrappers. Just standard Web3 patterns.
Migration Complexity: N/A (Don't migrate) Migration Risk: N/A (Use as reference only) Value: High as learning resource, documentation, and pattern reference
The main value is understanding what worked and what was over-engineered, so we can build nft-launchpad better from the start.
