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

Gbolahan AkandeGbolahan Akande
9 min read

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 (Hiro, 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 (Hiro, 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:

2
Subscribe to my newsletter

Read articles from Gbolahan Akande directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gbolahan Akande
Gbolahan Akande