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 ā