Day 2: Building a Modern React Frontend for Your Smart Contract

Table of contents
- What You'll Learn Today
- Before We Start
- Understanding Modern Web3 Frontend Architecture
- Setting Up Your React Environment
- Understanding Stacks.js 8.x Changes
- Building Your First Blockchain Component
- Building the User Interface
- Running Your dApp
- Key Concepts You Learned
- Common Issues and Solutions
- Tomorrow's Preview
- Join the Community
- Complete Implementation

Transform your command-line smart contract into an interactive web application using React 19, Next.js 15, and the latest Stacks.js with SIP-030 wallet connections!
Yesterday, you wrote your first Clarity contract. Today, we’ll bring it to life with a frontend.
We’ll build a React + Next.js app that connects to your deployed smart contract. By the end of this tutorial, you’ll have a web interface that can:
Connect to a Stacks wallet (via the new SIP-030 standard)
Read smart contract data (like your greeting message)
Display real-time info from the blockchain
Let’s get started.
What You'll Learn Today
By the end of this tutorial, you'll understand:
How to set up React 19 with Next.js 15 for web3 development
The new Stacks.js 8.x wallet connection methods with SIP-030
How to read data from your Clarity 3.0 smart contract in a web app
Modern UI patterns for blockchain applications
Type-safe blockchain interactions with TypeScript
Before We Start
You'll need your deployed contract from Day 1. We're building a frontend that connects to your hello-world
contract, so make sure you have the contract address ready.
Understanding Modern Web3 Frontend Architecture
The 2025 Stack
Why This Combination?
React 19: Latest concurrent features and improved hooks
Next.js 15: App Router with server components and TypeScript
Stacks.js 8.x: New SIP-030 standard for wallet connections
TypeScript: Type safety for blockchain interactions
Tailwind CSS: Modern, utility-first styling
Key Concepts We'll Cover
Wallet Connection Flow:
User clicks "Connect Wallet"
Browser shows available Stacks wallets
User selects wallet (Hiro, Xverse, etc.)
App receives wallet address and capabilities
App can now read/write to blockchain
Reading vs Writing:
Reading data from the contract is free and instant.
Writing data (like submitting a transaction) costs STX and needs wallet approval.
Setting Up Your React Environment
Step 1: Create Next.js 15 Application
Use this command to scaffold your app with everything you need (TypeScript, Tailwind, etc.):
# Create a modern Next.js app with all the latest features
npx create-next-app@latest stacks-frontend \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*"
cd stacks-frontend
This gives you a clean and scalable app layout with modern features like app routing and import aliases.
Step 2: Install Stacks.js Dependencies
Install the core packages for interacting with the Stacks blockchain:
# Install the latest Stacks.js packages (2025 versions)
npm install @stacks/connect@latest @stacks/transactions@latest @stacks/network@latest
Understanding the Packages:
@stacks/connect
: Wallet connection and SIP-030 support@stacks/transactions
: Create and sign blockchain transactions@stacks/network
: Configure testnet/mainnet connections
Step 3: Environment Configuration
Create .env.local
for your environment variables:
env
NEXT_PUBLIC_STACKS_NETWORK=testnet
NEXT_PUBLIC_CONTRACT_ADDRESS=ST123...ABC.hello-world
Why Environment Variables?
Easy to switch between testnet/mainnet
Keep sensitive info secure
Different configs for different developers
Understanding Stacks.js 8.x Changes
If you’ve used earlier versions of Stacks.js, you’ll notice a huge improvement. Here’s a quick comparison:
The Old Way (Stacks.js 7.x)
javascript
// Old way - more complex, JWT-based
import { showConnect } from '@stacks/connect';
import { AppConfig, UserSession } from '@stacks/auth';
const appConfig = new AppConfig(['store_write', 'publish_data']);
const userSession = new UserSession({ appConfig });
showConnect({
appDetails: { name: 'My App' },
onFinish: () => { /* complex session handling */ },
userSession
});
The New Way (Stacks.js 8.x with SIP-030)
javascript
// New way - simpler, more standardized
import { connect } from '@stacks/connect';
const response = await connect({
onFinish: (data) => {
console.log('Connected!', data.addresses);
}
});
Why it’s better:
Simpler API with fewer concepts
Standardized across wallets (SIP-030)
Better TypeScript support
More reliable connection handling
Building Your First Blockchain Component
Step 1: Create a Stacks Configuration File
Create src/lib/stacks.ts
to store all your contract/network settings in one place.
This helps:
Avoid duplication
Type-safe environment variables
Easily reuse network and contract data across your app
import { StacksNetwork, StacksTestnet, StacksMainnet } from '@stacks/network';
// Network configuration
export const network: StacksNetwork =
process.env.NEXT_PUBLIC_STACKS_NETWORK === 'mainnet'
? new StacksMainnet()
: new StacksTestnet();
export const contractAddress = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS!;
export const contractName = 'hello-world';
// This will be used throughout your app
export const CONTRACT_PRINCIPAL = `${contractAddress}.${contractName}`;
Step 2: Understanding Wallet Connection
Create a simple wallet connection hook (src/hooks/useWallet.ts
):
We’ll create a custom hook useWallet
to manage wallet state.
It’ll handle:
Connecting and disconnecting
Storing the address in localStorage
Detecting existing sessions
'use client';
import { useState, useEffect } from 'react';
import { connect, disconnect, isConnected } from '@stacks/connect';
export function useWallet() {
const [address, setAddress] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
// Check for existing connection on page load
useEffect(() => {
if (isConnected()) {
// Get address from local storage (set by Stacks.js)
const savedAddress = localStorage.getItem('stacks-address');
setAddress(savedAddress);
}
}, []);
const connectWallet = async () => {
setIsLoading(true);
try {
await connect({
onFinish: (data) => {
const stxAddress = data.addresses.stx;
setAddress(stxAddress);
localStorage.setItem('stacks-address', stxAddress);
setIsLoading(false);
},
onCancel: () => {
setIsLoading(false);
}
});
} catch (error) {
console.error('Connection failed:', error);
setIsLoading(false);
}
};
const disconnectWallet = () => {
disconnect();
setAddress(null);
localStorage.removeItem('stacks-address');
};
return {
address,
isConnected: !!address,
connectWallet,
disconnectWallet,
isLoading
};
}
Key Concepts Explained:
State Management:
useState
for reactive address storageuseEffect
for checking existing connectionslocalStorage
for persistence
Connection Flow:
connect()
opens wallet selectoronFinish
receives wallet dataonCancel
handles user cancellation
Step 3: Reading Contract Data
Create a hook for contract interactions (src/hooks/useContract.ts
):
Next, we’ll create a useContractRead
hook to fetch values from your Clarity contract.
It uses callReadOnlyFunction
, so:
No wallet required
No STX fees
Works instantly
We also use cvToJSON
to convert raw Clarity values into readable JavaScript types.
'use client';
import { useState, useEffect } from 'react';
import { callReadOnlyFunction, cvToJSON } from '@stacks/transactions';
import { network, contractAddress, contractName } from '@/lib/stacks';
export function useContractRead(functionName: string) {
const [data, setData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchData() {
try {
setIsLoading(true);
const result = await callReadOnlyFunction({
contractAddress,
contractName,
functionName,
functionArgs: [],
network,
});
// Convert Clarity value to JavaScript
const jsonResult = cvToJSON(result);
setData(jsonResult.value);
setError(null);
} catch (err: any) {
setError(err.message);
console.error('Contract read error:', err);
} finally {
setIsLoading(false);
}
}
fetchData();
}, [functionName]);
return { data, isLoading, error };
}
Again understanding This Hook:
callReadOnlyFunction:
Calls contract functions without gas cost
Returns Clarity Value (CV) format
Doesn't require wallet connection
cvToJSON:
Converts Clarity Values to JavaScript objects
Handles Clarity types like
(ok ...)
,uint
,string-ascii
Makes data easy to use in React
Building the User Interface
Step 1: Create the Main Page Component
Update src/app/page.tsx
:
tsx
'use client';
import { useWallet } from '@/hooks/useWallet';
import { useContractRead } from '@/hooks/useContract';
export default function Home() {
const { address, isConnected, connectWallet, disconnectWallet, isLoading } = useWallet();
const { data: greeting, isLoading: greetingLoading } = useContractRead('get-greeting');
const { data: blockInfo, isLoading: blockLoading } = useContractRead('get-block-info');
return (
<div className="min-h-screen bg-gray-50 p-8">
<div className="max-w-2xl mx-auto space-y-6">
{/* Header */}
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900">
My First Stacks dApp
</h1>
<p className="text-gray-600 mt-2">
Connected to your Clarity 3.0 smart contract
</p>
</div>
{/* Wallet Connection */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">Wallet</h2>
{isConnected ? (
<div className="space-y-2">
<p className="text-sm text-gray-600">Connected Address:</p>
<p className="font-mono text-sm bg-gray-100 p-2 rounded">
{address}
</p>
<button
onClick={disconnectWallet}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
>
Disconnect
</button>
</div>
) : (
<button
onClick={connectWallet}
disabled={isLoading}
className="bg-blue-500 text-white px-6 py-3 rounded hover:bg-blue-600 disabled:opacity-50"
>
{isLoading ? 'Connecting...' : 'Connect Wallet'}
</button>
)}
</div>
{/* Contract Data */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">Contract Data</h2>
{/* Current Greeting */}
<div className="mb-4">
<h3 className="font-medium text-gray-700">Current Greeting:</h3>
{greetingLoading ? (
<p className="text-gray-500">Loading...</p>
) : (
<p className="text-lg font-semibold text-blue-600">
{greeting || 'No greeting found'}
</p>
)}
</div>
{/* Clarity 3.0 Block Info */}
<div>
<h3 className="font-medium text-gray-700 mb-2">Clarity 3.0 Block Info:</h3>
{blockLoading ? (
<p className="text-gray-500">Loading...</p>
) : blockInfo ? (
<div className="space-y-1 text-sm">
<p>Stacks Blocks: <span className="font-mono">{blockInfo['stacks-blocks']}</span></p>
<p>Tenure Blocks: <span className="font-mono">{blockInfo['tenure-blocks']}</span></p>
<p>Est. Time: <span className="font-mono">{blockInfo['estimated-time']}</span></p>
</div>
) : (
<p className="text-gray-500">No block info available</p>
)}
</div>
</div>
</div>
</div>
);
}
Step 2: Understanding the Component Structure
React Hooks Pattern:
tsx
// Custom hooks encapsulate blockchain logic
const { address, isConnected, connectWallet } = useWallet();
const { data: greeting, isLoading } = useContractRead('get-greeting');
Conditional Rendering:
tsx
{isConnected ? (
<div>Connected: {address}</div>
) : (
<button onClick={connectWallet}>Connect</button>
)}
Loading States:
tsx
{greetingLoading ? (
<p>Loading...</p>
) : (
<p>{greeting}</p>
)}
Running Your dApp
Step 1: Start Development Server
bash
npm run dev
Visit http://localhost:3000
to see your application!
Step 2: Test the Connection Flow
Click "Connect Wallet"
Browser opens wallet selector
Choose your Stacks wallet (Hiro, Xverse, etc.)
Approve the connection
View Contract Data
See your greeting from the smart contract
View Clarity 3.0 block information
Data updates automatically
Key Concepts You Learned
Modern Wallet Integration:
SIP-030 standard for cross-wallet compatibility
Simplified connection API in Stacks.js 8.x
Persistent connections with localStorage
Contract Reading:
callReadOnlyFunction
for free contract callscvToJSON
for data conversionReact hooks for state management
Type Safety:
TypeScript throughout the application
Proper error handling with try/catch
Environment variable validation
Modern React Patterns:
Custom hooks for reusable logic
Conditional rendering for UI states
Effect hooks for data fetching
Common Issues and Solutions
Connection Problems:
typescript
// Always handle connection errors
try {
await connect({ /* options */ });
} catch (error) {
console.error('Connection failed:', error);
// Show user-friendly error message
}
Data Loading:
typescript
// Always show loading states
{isLoading ? <Spinner /> : <Data />}
Environment Variables:
typescript
// Always validate required env vars
if (!process.env.NEXT_PUBLIC_CONTRACT_ADDRESS) {
throw new Error('Contract address required');
}
Tomorrow's Preview
Ready to add write functionality to your dApp? Tomorrow we're implementing:
Transaction signing with user wallets
Writing data to your smart contract
Transaction status tracking and confirmations
Error handling for failed transactions
Optimistic UI updates for better user experience
We'll turn your read-only dApp into a fully interactive blockchain application!
Join the Community
How did your first React + Stacks integration go? Share screenshots of your dApp in the comments! Having trouble with wallet connections or contract reads? Let us know - troubleshooting together makes us all better developers.
Complete Implementation
Remember, all the working code for today's concepts is available in our GitHub repository. The tutorial teaches you why and how - the repo shows you the complete implementation details.
Next up: [Day 3 - Adding Write Functionality and Transaction Management]
This is Day 2 of our 30-day Clarity & Stacks.js tutorial series. Each day builds on the previous, teaching you to create sophisticated decentralized applications step by step.
Essential Resources:
Subscribe to my newsletter
Read articles from Gbolahan Akande directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
