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.