Skip to content

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

Version: 0.0.1 Package Name: @emprops/web3Status: Reference only (not for migration) Last Updated: 2025-11-09


Table of Contents

  1. Overview
  2. Purpose & Role
  3. Architecture
  4. Key Patterns to Extract
  5. Provider System
  6. Adapter Pattern
  7. Hook Architecture
  8. Type System
  9. Environment Management
  10. Testing Approach
  11. Integration Recommendations
  12. 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.json

Technology Stack

json
{
  "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:

  1. ✅ Test the full stack (contracts → ponder → frontend)
  2. ✅ Validate data flow patterns
  3. ✅ Demonstrate dual data sources (on-chain vs indexed)
  4. ✅ 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:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
// 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:

typescript
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:

typescript
// 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 types

Example - Contract Types:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
// 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

typescript
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:

typescript
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:

  1. ❌ Over-abstraction - wraps Wagmi which already works well
  2. ❌ Extra layer of complexity - provider within provider within provider
  3. ❌ Contract info should come from API, not context
  4. ❌ Harder to debug than direct Wagmi usage

Better Approach for nft-launchpad

Just use Wagmi and RainbowKit directly:

typescript
// 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 reads
  • src/adapter/PonderAdapter.ts - API reads
  • src/adapter/types.ts - Shared interfaces

WagmiAdapter

Purpose: Read contract data directly from blockchain

typescript
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

typescript
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:

typescript
// 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:

typescript
// 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

typescript
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

typescript
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:

typescript
// 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:

typescript
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

typescript
export interface EmPropsApp {
  id: string;
  type: string;
  name: string;
  symbol: string;
  address: string;
  ownerTokenId: string;
  maxSupply: string;
  mintPrice: string;
  totalMinted: string;
}

For nft-launchpad:

typescript
// 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:

typescript
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:

typescript
// 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:

typescript
// 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

typescript
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

  1. Over-Abstraction

    • ❌ Don't wrap Wagmi hooks with custom hooks
    • ✅ Use Wagmi hooks directly
  2. Provider Inception

    • ❌ Don't create provider wrapping providers
    • ✅ Use Wagmi/RainbowKit providers directly
  3. Hardcoded Contract Info

    • ❌ Don't store ABIs in context
    • ✅ Fetch from API or import from package
  4. Generic Hook Wrappers

    • ❌ Don't create useEmPropsQuery wrapper
    • ✅ Create specific hooks like useCollections
  5. Adapter Classes for Simple Cases

    • ❌ Don't create adapter classes
    • ✅ Use functions: fetchFromAPI() and useReadContract()

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:

  1. ✅ Study and understand patterns
  2. ✅ Extract type definitions
  3. ✅ Copy React Query + Socket.io pattern
  4. ✅ Use environment validation approach
  5. ✅ Reference for testing strategies

DON'T:

  1. ❌ Migrate as a package
  2. ❌ Copy provider system
  3. ❌ Copy adapter classes
  4. ❌ Copy hook wrappers
  5. ❌ Add abstraction layers

For nft-launchpad

Build fresh with lessons learned:

typescript
// 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.

Released under the MIT License.