Skip to main content

Command Palette

Search for a command to run...

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

Updated
9 min read
Day 2: Building a Modern React Frontend for Your Smart Contract
G
AI automation specialist, data analyst and blockchain engineer

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:

  1. User clicks "Connect Wallet"

  2. Browser shows available Stacks wallets

  3. User selects wallet (Leather, Xverse, etc.)

  4. App receives wallet address and capabilities

  5. 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 storage

  • useEffect for checking existing connections

  • localStorage for persistence

Connection Flow:

  • connect() opens wallet selector

  • onFinish receives wallet data

  • onCancel 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

  1. Click "Connect Wallet"

    • Browser opens wallet selector

    • Choose your Stacks wallet (Leather, Xverse, etc.)

    • Approve the connection

  2. 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 calls

  • cvToJSON for data conversion

  • React 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:

30 Days of Clarity & Stacks.js: From Zero to DeFi

Part 2 of 5

Learn blockchain development on Stacks! This tutorial series takes you from writing your first "Hello World" Clarity smart contract to building sophisticated DeFi applications with frontend integration. Learn and have the skills to build apps.

Up next

Making Your dApp Interactive - Understanding Blockchain Writing

Today we're crossing a major milestone: transforming your read-only app into one that can actually change data on the blockchain. This is where things get exciting - and a bit more complex! What You'll Learn Today Today's journey will help you unders...