Back to Blog
Blockchain

Building Your First Web3 DApp: Complete Guide

Cap
5 min read
web3dappethereumreact

Building Your First Web3 DApp: Complete Guide

From zero to deployed DApp in under 2 hours

šŸŽÆ What We're Building

A decentralized voting system with:

  • āœ… Smart contract on Ethereum
  • āœ… React frontend with Web3 integration
  • āœ… MetaMask wallet connection
  • āœ… Real-time vote tracking

šŸ”§ Tech Stack

  • Backend: Solidity + Hardhat
  • Frontend: React + Ethers.js + Tailwind
  • Network: Sepolia testnet
  • Wallet: MetaMask integration

šŸ“ Smart Contract

// contracts/Voting.sol
pragma solidity ^0.8.19;

contract Voting {
    struct Proposal {
        string description;
        uint256 voteCount;
        bool exists;
    }
    
    mapping(uint256 => Proposal) public proposals;
    mapping(address => mapping(uint256 => bool)) public hasVoted;
    uint256 public proposalCount;
    address public owner;
    
    event ProposalCreated(uint256 indexed proposalId, string description);
    event VoteCast(address indexed voter, uint256 indexed proposalId);
    
    constructor() {
        owner = msg.sender;
    }
    
    function createProposal(string memory _description) external {
        require(msg.sender == owner, "Only owner can create proposals");
        
        proposalCount++;
        proposals[proposalCount] = Proposal({
            description: _description,
            voteCount: 0,
            exists: true
        });
        
        emit ProposalCreated(proposalCount, _description);
    }
    
    function vote(uint256 _proposalId) external {
        require(proposals[_proposalId].exists, "Proposal doesn't exist");
        require(!hasVoted[msg.sender][_proposalId], "Already voted");
        
        proposals[_proposalId].voteCount++;
        hasVoted[msg.sender][_proposalId] = true;
        
        emit VoteCast(msg.sender, _proposalId);
    }
    
    function getProposal(uint256 _proposalId) external view returns (
        string memory description,
        uint256 voteCount
    ) {
        require(proposals[_proposalId].exists, "Proposal doesn't exist");
        
        Proposal memory proposal = proposals[_proposalId];
        return (proposal.description, proposal.voteCount);
    }
}

āš›ļø React Frontend

// src/App.js
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import VotingABI from './contracts/Voting.json';

const VOTING_ADDRESS = "0x..."; // Your deployed contract address

function App() {
  const [account, setAccount] = useState('');
  const [contract, setContract] = useState(null);
  const [proposals, setProposals] = useState([]);
  const [newProposal, setNewProposal] = useState('');
  const [loading, setLoading] = useState(false);

  // Connect MetaMask
  const connectWallet = async () => {
    if (window.ethereum) {
      try {
        const accounts = await window.ethereum.request({
          method: 'eth_requestAccounts'
        });
        setAccount(accounts[0]);
        
        const provider = new ethers.providers.Web3Provider(window.ethereum);
        const signer = provider.getSigner();
        const votingContract = new ethers.Contract(
          VOTING_ADDRESS,
          VotingABI.abi,
          signer
        );
        setContract(votingContract);
        
        // Load proposals
        loadProposals(votingContract);
      } catch (error) {
        console.error('Error connecting wallet:', error);
      }
    } else {
      alert('Please install MetaMask!');
    }
  };

  // Load all proposals
  const loadProposals = async (contractInstance) => {
    try {
      const proposalCount = await contractInstance.proposalCount();
      const proposalsList = [];
      
      for (let i = 1; i <= proposalCount; i++) {
        const proposal = await contractInstance.getProposal(i);
        proposalsList.push({
          id: i,
          description: proposal[0],
          voteCount: proposal[1].toNumber()
        });
      }
      
      setProposals(proposalsList);
    } catch (error) {
      console.error('Error loading proposals:', error);
    }
  };

  // Create new proposal
  const createProposal = async () => {
    if (!contract || !newProposal.trim()) return;
    
    try {
      setLoading(true);
      const tx = await contract.createProposal(newProposal);
      await tx.wait();
      
      setNewProposal('');
      loadProposals(contract);
    } catch (error) {
      console.error('Error creating proposal:', error);
    } finally {
      setLoading(false);
    }
  };

  // Vote on proposal
  const vote = async (proposalId) => {
    if (!contract) return;
    
    try {
      setLoading(true);
      const tx = await contract.vote(proposalId);
      await tx.wait();
      
      loadProposals(contract);
    } catch (error) {
      console.error('Error voting:', error);
      alert('Error: ' + error.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="min-h-screen bg-gray-100 py-8">
      <div className="max-w-4xl mx-auto px-4">
        <h1 className="text-4xl font-bold text-center mb-8">
          Decentralized Voting DApp
        </h1>
        
        {/* Wallet Connection */}
        {!account ? (
          <div className="text-center">
            <button
              onClick={connectWallet}
              className="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600"
            >
              Connect MetaMask
            </button>
          </div>
        ) : (
          <div className="space-y-6">
            <div className="bg-white p-4 rounded-lg shadow">
              <p className="text-sm text-gray-600">Connected Account:</p>
              <p className="font-mono text-sm">{account}</p>
            </div>
            
            {/* Create Proposal */}
            <div className="bg-white p-6 rounded-lg shadow">
              <h2 className="text-xl font-semibold mb-4">Create Proposal</h2>
              <div className="flex gap-4">
                <input
                  type="text"
                  value={newProposal}
                  onChange={(e) => setNewProposal(e.target.value)}
                  placeholder="Enter proposal description"
                  className="flex-1 p-2 border rounded"
                />
                <button
                  onClick={createProposal}
                  disabled={loading}
                  className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 disabled:opacity-50"
                >
                  {loading ? 'Creating...' : 'Create'}
                </button>
              </div>
            </div>
            
            {/* Proposals List */}
            <div className="space-y-4">
              <h2 className="text-xl font-semibold">Active Proposals</h2>
              {proposals.map((proposal) => (
                <div key={proposal.id} className="bg-white p-6 rounded-lg shadow">
                  <div className="flex justify-between items-start">
                    <div className="flex-1">
                      <h3 className="font-semibold mb-2">
                        Proposal #{proposal.id}
                      </h3>
                      <p className="text-gray-700 mb-4">
                        {proposal.description}
                      </p>
                      <p className="text-sm text-gray-500">
                        Votes: {proposal.voteCount}
                      </p>
                    </div>
                    <button
                      onClick={() => vote(proposal.id)}
                      disabled={loading}
                      className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 disabled:opacity-50"
                    >
                      Vote
                    </button>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

export default App;

šŸš€ Deployment Script

// scripts/deploy.js
const hre = require("hardhat");

async function main() {
  const Voting = await hre.ethers.getContractFactory("Voting");
  const voting = await Voting.deploy();

  await voting.deployed();

  console.log("Voting deployed to:", voting.address);
  
  // Create sample proposals
  await voting.createProposal("Should we implement feature X?");
  await voting.createProposal("Increase community rewards?");
  
  console.log("Sample proposals created!");
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

šŸ“‹ Quick Setup

# 1. Initialize project
mkdir voting-dapp && cd voting-dapp
npx create-react-app frontend
mkdir contracts

# 2. Install dependencies
npm install --save-dev hardhat @nomiclabs/hardhat-ethers ethers
npm install ethers

# 3. Deploy contract
npx hardhat run scripts/deploy.js --network sepolia

# 4. Update frontend with contract address
# 5. Start frontend
cd frontend && npm start

šŸŽÆ Key Features Implemented

āœ… Wallet Connection: MetaMask integration āœ… Smart Contract Interaction: Read/write operations
āœ… Real-time Updates: Automatic refresh after transactions āœ… Error Handling: User-friendly error messages āœ… Responsive Design: Works on mobile and desktop

šŸ“Š Production Considerations

Security:

  • Input validation
  • Access controls
  • Reentrancy protection

UX Improvements:

  • Loading states
  • Transaction confirmations
  • Error handling

Scalability:

  • Event indexing
  • Pagination for large datasets
  • Caching layer

Ready to build more complex DApps? This foundation covers 80% of common Web3 integration patterns.

WY

Cap

Senior Golang Backend & Web3 Developer with 10+ years of experience building scalable systems and blockchain solutions.

View Full Profile →