React & Wagmi Integration
The Grapevine SDK provides first-class support for React applications with wagmi wallet integration. Build decentralized applications where users connect their own wallets.
Installation
npm install @pinata/grapevine-sdk wagmi viem @tanstack/react-queryQuick Start
1. Configure Wagmi
Set up wagmi with your preferred chains and connectors:
import { createConfig, http } from 'wagmi';
import { base, baseSepolia } from 'wagmi/chains';
import { injected, walletConnect } from 'wagmi/connectors';
const projectId = 'YOUR_WALLET_CONNECT_PROJECT_ID';
export const config = createConfig({
chains: [base, baseSepolia],
connectors: [
injected(),
walletConnect({ projectId }),
],
transports: {
[base.id]: http(),
[baseSepolia.id]: http(),
},
});2. Wrap Your App
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from './wagmi.config';
const queryClient = new QueryClient();
function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
</WagmiProvider>
);
}3. Use Grapevine with Wagmi
import { useGrapevine, useGrapevineReady } from '@pinata/grapevine-sdk/react';
import { useWalletClient, useAccount, useConnect, useDisconnect } from 'wagmi';
import { injected } from 'wagmi/connectors';
function GrapevineFeeds() {
const { address, isConnected } = useAccount();
const { data: walletClient } = useWalletClient();
const { connect } = useConnect();
const { disconnect } = useDisconnect();
// Initialize Grapevine with wagmi
const grapevine = useGrapevine({
walletClient,
network: 'testnet', // or 'mainnet'
debug: true
});
// Check if Grapevine is ready
const isReady = useGrapevineReady(grapevine);
const [feeds, setFeeds] = useState([]);
const loadFeeds = async () => {
if (!grapevine || !isReady) return;
const result = await grapevine.feeds.list({ page_size: 10 });
setFeeds(result.data);
};
const createFeed = async () => {
if (!grapevine || !isReady) return;
const feed = await grapevine.feeds.create({
name: `Feed ${Date.now()}`,
description: 'Created with wagmi',
tags: ['demo', 'wagmi']
});
console.log('Created feed:', feed);
await loadFeeds(); // Refresh list
};
if (!isConnected) {
return (
<button onClick={() => connect({ connector: injected() })}>
Connect Wallet
</button>
);
}
return (
<div>
<p>Connected: {address}</p>
<button onClick={() => disconnect()}>Disconnect</button>
{isReady ? (
<>
<button onClick={createFeed}>Create Feed</button>
<button onClick={loadFeeds}>Load Feeds</button>
<ul>
{feeds.map(feed => (
<li key={feed.id}>{feed.name}</li>
))}
</ul>
</>
) : (
<p>Initializing Grapevine...</p>
)}
</div>
);
}React Hooks
useGrapevine
Main hook for creating a Grapevine client with wagmi integration.
const grapevine = useGrapevine({
walletClient, // From useWalletClient()
network, // 'testnet' | 'mainnet'
debug? // Enable debug logging
});Note: The wallet address is automatically extracted from walletClient.account.address.
Returns: GrapevineClient | null
useGrapevineReady
Check if the Grapevine client is initialized and ready to use.
const isReady = useGrapevineReady(grapevine);Returns: boolean - true if client is not null
useGrapevineWalletReady
Check if the Grapevine client has a wallet configured and is ready for authenticated requests.
const hasWallet = useGrapevineWalletReady(grapevine);Returns: boolean - true if client exists AND has a wallet configured
import { useGrapevine, useGrapevineReady, useGrapevineWalletReady } from '@pinata/grapevine-sdk/react';
import { useWalletClient } from 'wagmi';
function MyComponent() {
const { data: walletClient } = useWalletClient();
const grapevine = useGrapevine({ walletClient, network: 'testnet' });
const isReady = useGrapevineReady(grapevine); // Client initialized
const hasWallet = useGrapevineWalletReady(grapevine); // Wallet connected
// Reading data - only need isReady
const loadFeeds = async () => {
if (!isReady) return;
const feeds = await grapevine.feeds.list();
};
// Writing data - need hasWallet
const createFeed = async () => {
if (!hasWallet) {
alert('Please connect your wallet');
return;
}
const feed = await grapevine.feeds.create({ name: 'My Feed' });
};
return (
<div>
<button onClick={loadFeeds} disabled={!isReady}>
Load Feeds (Public)
</button>
<button onClick={createFeed} disabled={!hasWallet}>
Create Feed (Auth Required)
</button>
</div>
);
}Complete Example
Here's a full-featured component with wallet connection, feed management, and entry creation:
import React, { useState, useEffect } from 'react';
import { useGrapevine, useGrapevineReady } from '@pinata/grapevine-sdk/react';
import {
useAccount,
useWalletClient,
useConnect,
useDisconnect,
useChainId,
useSwitchChain
} from 'wagmi';
import { injected } from 'wagmi/connectors';
import { baseSepolia } from 'wagmi/chains';
export function GrapevineManager() {
const { address, isConnected } = useAccount();
const { data: walletClient } = useWalletClient();
const chainId = useChainId();
const { switchChain } = useSwitchChain();
const { connect } = useConnect();
const { disconnect } = useDisconnect();
// Initialize Grapevine
const grapevine = useGrapevine({
walletClient,
network: 'testnet'
});
const isReady = useGrapevineReady(grapevine);
// State
const [feeds, setFeeds] = useState([]);
const [selectedFeed, setSelectedFeed] = useState(null);
const [entries, setEntries] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Check if on correct chain
const isCorrectChain = chainId === baseSepolia.id;
// Load user's feeds
const loadMyFeeds = async () => {
if (!grapevine || !isReady) return;
setLoading(true);
setError(null);
try {
const result = await grapevine.feeds.myFeeds();
setFeeds(result.data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
// Create a new feed
const createFeed = async () => {
if (!grapevine || !isReady) return;
const name = prompt('Feed name:');
if (!name) return;
setLoading(true);
setError(null);
try {
const feed = await grapevine.feeds.create({
name,
description: prompt('Feed description:') || '',
tags: (prompt('Tags (comma-separated):') || '').split(',').filter(Boolean)
});
console.log('Created feed:', feed);
await loadMyFeeds();
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
// Load entries for a feed
const loadEntries = async (feedId) => {
if (!grapevine || !isReady) return;
setLoading(true);
setError(null);
try {
const result = await grapevine.entries.list(feedId, { page_size: 20 });
setEntries(result.data);
setSelectedFeed(feedId);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
// Add an entry to the selected feed
const addEntry = async () => {
if (!grapevine || !isReady || !selectedFeed) return;
const content = prompt('Entry content:');
if (!content) return;
setLoading(true);
setError(null);
try {
const entry = await grapevine.entries.create(selectedFeed, {
content,
title: prompt('Entry title:') || undefined,
description: prompt('Entry description:') || undefined,
is_free: confirm('Make this entry free?'),
tags: (prompt('Tags (comma-separated):') || '').split(',').filter(Boolean)
});
console.log('Created entry:', entry);
await loadEntries(selectedFeed);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
// Auto-load feeds when ready
useEffect(() => {
if (isReady) {
loadMyFeeds();
}
}, [isReady]);
// Render wallet connection
if (!isConnected) {
return (
<div className="p-4">
<h2 className="text-xl mb-4">Connect Your Wallet</h2>
<button
onClick={() => connect({ connector: injected() })}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
Connect Wallet
</button>
</div>
);
}
// Check chain
if (!isCorrectChain) {
return (
<div className="p-4">
<h2 className="text-xl mb-4">Wrong Network</h2>
<p>Please switch to Base Sepolia</p>
<button
onClick={() => switchChain({ chainId: baseSepolia.id })}
className="px-4 py-2 bg-orange-500 text-white rounded"
>
Switch Network
</button>
</div>
);
}
// Render main UI
return (
<div className="p-4 max-w-6xl mx-auto">
{/* Header */}
<div className="mb-6 p-4 bg-gray-100 rounded">
<h1 className="text-2xl font-bold mb-2">Grapevine Manager</h1>
<div className="flex justify-between items-center">
<div>
<p className="text-sm text-gray-600">Wallet: {address?.slice(0, 8)}...</p>
<p className="text-sm text-gray-600">Network: Base Sepolia</p>
<p className="text-sm text-gray-600">
Status: {isReady ? '✅ Ready' : '⏳ Initializing...'}
</p>
</div>
<button
onClick={() => disconnect()}
className="px-3 py-1 bg-red-500 text-white rounded text-sm"
>
Disconnect
</button>
</div>
</div>
{/* Error display */}
{error && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
Error: {error}
</div>
)}
{/* Loading indicator */}
{loading && (
<div className="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
Loading...
</div>
)}
{/* Main content */}
<div className="grid grid-cols-2 gap-6">
{/* Feeds panel */}
<div className="border rounded p-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">My Feeds</h2>
<button
onClick={createFeed}
disabled={!isReady || loading}
className="px-3 py-1 bg-green-500 text-white rounded text-sm disabled:opacity-50"
>
+ Create Feed
</button>
</div>
<div className="space-y-2">
{feeds.length === 0 ? (
<p className="text-gray-500">No feeds yet</p>
) : (
feeds.map(feed => (
<div
key={feed.id}
onClick={() => loadEntries(feed.id)}
className={`p-3 border rounded cursor-pointer hover:bg-gray-50 ${
selectedFeed === feed.id ? 'bg-blue-50 border-blue-500' : ''
}`}
>
<h3 className="font-medium">{feed.name}</h3>
<p className="text-sm text-gray-600">{feed.description}</p>
<p className="text-xs text-gray-500">
{feed.total_entries} entries • {feed.is_active ? 'Active' : 'Inactive'}
</p>
</div>
))
)}
</div>
</div>
{/* Entries panel */}
<div className="border rounded p-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">
{selectedFeed ? 'Feed Entries' : 'Select a Feed'}
</h2>
{selectedFeed && (
<button
onClick={addEntry}
disabled={!isReady || loading}
className="px-3 py-1 bg-green-500 text-white rounded text-sm disabled:opacity-50"
>
+ Add Entry
</button>
)}
</div>
<div className="space-y-2">
{!selectedFeed ? (
<p className="text-gray-500">Select a feed to view entries</p>
) : entries.length === 0 ? (
<p className="text-gray-500">No entries in this feed</p>
) : (
entries.map(entry => (
<div key={entry.id} className="p-3 border rounded">
<h3 className="font-medium">{entry.title || 'Untitled'}</h3>
<p className="text-sm text-gray-600 line-clamp-2">
{entry.description || 'No description'}
</p>
<p className="text-xs text-gray-500">
{entry.is_free ? '🆓 Free' : '💰 Paid'} •
{entry.mime_type} •
{new Date(entry.created_at * 1000).toLocaleDateString()}
</p>
</div>
))
)}
</div>
</div>
</div>
</div>
);
}Advanced Patterns
Error Handling
const [error, setError] = useState<Error | null>(null);
const handleGrapevineAction = async () => {
try {
setError(null);
const result = await grapevine.feeds.create({ name: 'Test' });
// Handle success
} catch (err) {
setError(err);
// Check specific error types
if (err.message.includes('402')) {
console.log('Payment required');
} else if (err.message.includes('401')) {
console.log('Authentication failed');
}
}
};Pagination with React Query
import { useInfiniteQuery } from '@tanstack/react-query';
function useFeedEntries(grapevine, feedId) {
return useInfiniteQuery({
queryKey: ['entries', feedId],
queryFn: async ({ pageParam }) => {
if (!grapevine) throw new Error('Grapevine not initialized');
return grapevine.entries.list(feedId, {
page_token: pageParam,
page_size: 20
});
},
initialPageParam: undefined,
getNextPageParam: (lastPage) => {
if (lastPage.has_more) {
return lastPage.next_page_token;
}
return undefined;
},
enabled: !!grapevine && !!feedId,
});
}Auto-refresh with SWR
import useSWR from 'swr';
function useMyFeeds(grapevine) {
const { data, error, mutate } = useSWR(
grapevine ? 'my-feeds' : null,
() => grapevine.feeds.myFeeds(),
{
refreshInterval: 30000, // Refresh every 30 seconds
revalidateOnFocus: true,
}
);
return {
feeds: data || [],
isLoading: !error && !data,
isError: error,
refresh: mutate
};
}TypeScript Support
The SDK provides full TypeScript support:
import type { Feed, Entry, GrapevineClient } from '@pinata/grapevine-sdk';
interface GrapevineContextValue {
client: GrapevineClient | null;
isReady: boolean;
feeds: Feed[];
entries: Map<string, Entry[]>;
}Testing
Mock wagmi hooks for testing:
// __tests__/GrapevineComponent.test.tsx
import { renderHook } from '@testing-library/react';
import { useGrapevine } from '@pinata/grapevine-sdk/react';
jest.mock('wagmi', () => ({
useWalletClient: () => ({ data: mockWalletClient }),
useAccount: () => ({ address: '0x123...', isConnected: true }),
}));
test('initializes grapevine client', () => {
const { result } = renderHook(() =>
useGrapevine({
walletClient: mockWalletClient,
network: 'testnet'
})
);
expect(result.current).toBeDefined();
});Common Issues
Wallet Not Connecting
Ensure wagmi is properly configured with supported chains:
// Must include the chain you're using
const config = createConfig({
chains: [base, baseSepolia], // Include both for flexibility
// ...
});Client Not Initializing
Check that walletClient is available (address is automatically extracted from walletClient.account.address):
const grapevine = useGrapevine({
walletClient, // Must be defined
network: 'testnet'
});
// Debug
console.log('Wallet client:', walletClient);
console.log('Grapevine ready:', isReady);Wrong Network
Handle network switching:
const { switchChain } = useSwitchChain();
const chainId = useChainId();
// Base Sepolia = 84532, Base = 8453
const correctChainId = network === 'testnet' ? 84532 : 8453;
if (chainId !== correctChainId) {
switchChain({ chainId: correctChainId });
}Next Steps
- Configuration - More configuration options
- Examples - Complete example applications
- API Reference - Full API documentation