Decentralized applications (dApps) are applications that run on a blockchain or peer-to-peer network of computers instead of a single computer. Let's learn how to build complete dApps from scratch.
What Makes a dApp?
A dApp has these key characteristics:
- Open source code
- Data stored on blockchain
- Cryptographic tokens for operation
- Uses blockchain for consensus
- No central authority controlling it
dApp Architecture
┌─────────────┐
│ Frontend │ (React/Vue/etc.)
├─────────────┤
│ Web3.js / │
│ Ethers.js │
├─────────────┤
│ Provider │ (MetaMask/WalletConnect)
├─────────────┤
│ Blockchain │ (Ethereum/etc.)
└─────────────┘Setting Up a dApp Project
Project Structure
my-dapp/
├── contracts/ # Smart contracts
├── src/
│ ├── components/ # React components
│ ├── hooks/ # Custom hooks
│ ├── utils/ # Web3 utilities
│ └── App.jsx
├── public/
│ └── index.html
├── package.json
├── hardhat.config.js
└── .envInitialize Project
bash
mkdir my-dapp
cd my-dapp
npm init -y
npm install --save-dev hardhat
npm install ethers@5 @openzeppelin/contracts
npx hardhatSmart Contract Development
Simple Storage Contract
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
contract SimpleStorage is Ownable {
struct Item {
uint256 id;
string name;
uint256 value;
address creator;
}
mapping(uint256 => Item) public items;
uint256 public itemCount;
event ItemCreated(
uint256 indexed id,
string name,
uint256 value,
address creator
);
event ItemUpdated(
uint256 indexed id,
string name,
uint256 value
);
function createItem(
string memory _name,
uint256 _value
) public {
itemCount++;
items[itemCount] = Item(
itemCount,
_name,
_value,
msg.sender
);
emit ItemCreated(itemCount, _name, _value, msg.sender);
}
function updateItem(
uint256 _id,
string memory _name,
uint256 _value
) public {
require(items[_id].creator == msg.sender, "Not owner");
items[_id].name = _name;
items[_id].value = _value;
emit ItemUpdated(_id, _name, _value);
}
function getItem(uint256 _id)
public
view
returns (
uint256 id,
string memory name,
uint256 value,
address creator
)
{
Item memory item = items[_id];
return (item.id, item.name, item.value, item.creator);
}
function getAllItems()
public
view
returns (Item[] memory)
{
Item[] memory allItems = new Item[](itemCount);
for (uint256 i = 0; i < itemCount; i++) {
allItems[i] = items[i + 1];
}
return allItems;
}
}Frontend with React
Install Dependencies
bash
npm install react react-dom
npm install ethers
npm install --save-dev vite @vitejs/plugin-reactWeb3 Provider Setup
jsx
// src/Web3Provider.jsx
import { createContext, useContext, useState, useEffect } from 'react';
import { ethers } from 'ethers';
const Web3Context = createContext();
export const useWeb3 = () => {
const context = useContext(Web3Context);
if (!context) {
throw new Error('useWeb3 must be used within Web3Provider');
}
return context;
};
export const Web3Provider = ({ children }) => {
const [account, setAccount] = useState(null);
const [provider, setProvider] = useState(null);
const [signer, setSigner] = useState(null);
const [chainId, setChainId] = useState(null);
const connectWallet = async () => {
if (window.ethereum) {
try {
const provider = new ethers.providers.Web3Provider(
window.ethereum
);
await provider.send('eth_requestAccounts', []);
const signer = provider.getSigner();
const address = await signer.getAddress();
const network = await provider.getNetwork();
setProvider(provider);
setSigner(signer);
setAccount(address);
setChainId(network.chainId);
} catch (error) {
console.error('Error connecting wallet:', error);
}
} else {
alert('Please install MetaMask!');
}
};
const disconnectWallet = () => {
setAccount(null);
setProvider(null);
setSigner(null);
setChainId(null);
};
useEffect(() => {
if (window.ethereum) {
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
disconnectWallet();
} else {
setAccount(accounts[0]);
}
});
window.ethereum.on('chainChanged', () => {
window.location.reload();
});
}
}, []);
return (
<Web3Context.Provider
value={{
account,
provider,
signer,
chainId,
connectWallet,
disconnectWallet
}}
>
{children}
</Web3Context.Provider>
);
};Contract Interaction Hook
jsx
// src/hooks/useContract.js
import { useState, useEffect } from 'react';
import { useWeb3 } from './useWeb3';
const CONTRACT_ADDRESS = '0x...';
const CONTRACT_ABI = [...];
export const useContract = () => {
const { signer, provider } = useWeb3();
const [contract, setContract] = useState(null);
useEffect(() => {
if (signer) {
const contractInstance = new ethers.Contract(
CONTRACT_ADDRESS,
CONTRACT_ABI,
signer
);
setContract(contractInstance);
}
}, [signer]);
return contract;
};Create Item Component
jsx
// src/components/CreateItem.jsx
import { useState } from 'react';
import { useWeb3 } from '../useWeb3';
import { useContract } from '../hooks/useContract';
export const CreateItem = () => {
const { account } = useWeb3();
const contract = useContract();
const [name, setName] = useState('');
const [value, setValue] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!contract || !account) {
alert('Please connect wallet');
return;
}
setLoading(true);
try {
const tx = await contract.createItem(name, value);
await tx.wait();
alert('Item created!');
setName('');
setValue('');
} catch (error) {
console.error('Error:', error);
alert('Error creating item');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Item name"
/>
<input
type="number"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Value"
/>
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Item'}
</button>
</form>
);
};Display Items Component
jsx
// src/components/ItemList.jsx
import { useState, useEffect } from 'react';
import { useContract } from '../hooks/useContract';
export const ItemList = () => {
const contract = useContract();
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadItems();
}, [contract]);
const loadItems = async () => {
if (!contract) return;
try {
const allItems = await contract.getAllItems();
setItems(allItems);
} catch (error) {
console.error('Error loading items:', error);
} finally {
setLoading(false);
}
};
if (loading) return <div>Loading...</div>;
return (
<div>
{items.map((item) => (
<div key={item.id.toString()}>
<h3>{item.name}</h3>
<p>Value: {item.value.toString()}</p>
<p>Creator: {item.creator}</p>
</div>
))}
</div>
);
};Deploying Your dApp
Deploy Contract
bash
npx hardhat compile
npx hardhat run scripts/deploy.js --network mainnetDeploy Frontend
bash
npm run build
# Deploy to IPFS, Vercel, Netlify, etc.Best Practices
- 1Always handle connection states
- 2Show loading indicators during transactions
- 3Handle errors gracefully
- 4Validate inputs before transactions
- 5Use events for real-time updates
- 6Implement gas estimation
- 7Support multiple networks
- 8Test thoroughly on testnets
Conclusion
Building dApps requires understanding both frontend development and blockchain interaction. With the right tools and patterns, you can create powerful decentralized applications that are transparent, secure, and user-controlled.