name: scaffold-eth-patterns description: Use when building with Scaffold-ETH 2. Covers project structure, custom hooks (useScaffoldReadContract, useScaffoldWriteContract), debug page, deploying contracts, hot reload, and wagmi integration.
Scaffold-ETH 2 Patterns
Overview
Scaffold-ETH 2 is an open-source toolkit for rapid dApp prototyping. It combines Foundry (or Hardhat) + Next.js + wagmi + RainbowKit with custom hooks that simplify contract interaction. Key difference from raw wagmi: Scaffold hooks auto-detect ABIs from deployed contracts and wait for transaction confirmations.
Quick Start
npx create-eth@latest my-dapp
cd my-dapp
yarn install
Start all services:
# Terminal 1: Local chain
yarn chain
# Terminal 2: Deploy contracts
yarn deploy
# Terminal 3: Start frontend
yarn start
Project Structure
my-dapp/
├── packages/
│ ├── foundry/ # Smart contracts
│ │ ├── contracts/ # Solidity sources
│ │ ├── script/ # Deploy scripts
│ │ ├── test/ # Contract tests
│ │ └── foundry.toml
│ └── nextjs/ # Frontend
│ ├── app/ # Next.js app router pages
│ ├── components/ # React components
│ ├── contracts/ # Auto-generated contract data
│ ├── hooks/scaffold-eth/ # Custom hooks
│ ├── scaffold.config.ts # Global config
│ └── utils/scaffold-eth/ # Utilities
├── package.json
└── yarn.lock
Custom Hooks
useScaffoldReadContract
Reads contract state with auto-detected ABI:
import { useScaffoldReadContract } from "~~/hooks/scaffold-eth";
function GreeterDisplay() {
const { data: greeting, isLoading } = useScaffoldReadContract({
contractName: "YourContract",
functionName: "greeting",
});
// With arguments
const { data: balance } = useScaffoldReadContract({
contractName: "YourContract",
functionName: "balanceOf",
args: ["0xAddress"],
});
if (isLoading) return <p>Loading...</p>;
return <p>Greeting: {greeting}</p>;
}
useScaffoldWriteContract
Writes to contracts with automatic confirmation waiting:
import { useScaffoldWriteContract } from "~~/hooks/scaffold-eth";
function SetGreeting() {
const { writeContractAsync, isPending } = useScaffoldWriteContract("YourContract");
const handleSubmit = async () => {
try {
// This waits for the tx to be confirmed (unlike raw wagmi)
await writeContractAsync({
functionName: "setGreeting",
args: ["Hello from Scaffold-ETH!"],
value: parseEther("0.01"), // Optional ETH value
});
console.log("Transaction confirmed!");
} catch (e) {
console.error("Transaction failed:", e);
}
};
return (
<button onClick={handleSubmit} disabled={isPending}>
{isPending ? "Sending..." : "Set Greeting"}
</button>
);
}
useScaffoldContract
Get a contract instance for direct interaction:
import { useScaffoldContract } from "~~/hooks/scaffold-eth";
function ContractInfo() {
const { data: contract } = useScaffoldContract({
contractName: "YourContract",
});
// Access contract address and ABI
console.log("Address:", contract?.address);
console.log("ABI:", contract?.abi);
}
useScaffoldEventHistory
Read historical events:
import { useScaffoldEventHistory } from "~~/hooks/scaffold-eth";
function EventLog() {
const { data: events, isLoading } = useScaffoldEventHistory({
contractName: "YourContract",
eventName: "GreetingChange",
fromBlock: 0n,
watch: true, // Live updates
});
return (
<ul>
{events?.map((event, i) => (
<li key={i}>
{event.args.greetingSetter}: {event.args.newGreeting}
</li>
))}
</ul>
);
}
Deploy Scripts
// packages/foundry/script/Deploy.s.sol
pragma solidity ^0.8.20;
import {ScaffoldETHDeploy} from "./DeployHelpers.s.sol";
import {YourContract} from "../contracts/YourContract.sol";
contract DeployScript is ScaffoldETHDeploy {
function run() external ScaffoldEthDeployerRunner {
YourContract yourContract = new YourContract(deployer);
console.logString(string.concat("YourContract deployed at: ", vm.toString(address(yourContract))));
}
}
# Deploy to local chain
yarn deploy
# Deploy to a specific network
yarn deploy --network base
# Reset deployments
yarn deploy --reset
Debug Page
Scaffold-ETH includes a debug page at /debug that auto-generates a UI for all contract functions:
- Lists all read functions with call buttons
- Lists all write functions with input fields
- Displays events in real-time
- Shows contract address and balance
- Works with any deployed contract (auto-detected from ABI)
Access at: http://localhost:3000/debug
Hot Reload
Scaffold-ETH watches for contract changes:
- Edit Solidity in
packages/foundry/contracts/ - Run
yarn deploy— recompiles and redeploys - Frontend auto-detects new ABI and addresses
- Debug page updates immediately
The deployedContracts.ts file is auto-generated on each deploy.
Scaffold-ETH vs Raw Wagmi
| Feature | Scaffold-ETH | Raw Wagmi |
|---|---|---|
| ABI management | Auto-detected | Manual import |
| Tx confirmation | Auto-waits | Manual useWaitForTransactionReceipt |
| Contract address | Auto-resolved | Manual address |
| Debug UI | Built-in /debug |
Build your own |
| Hot reload | Contracts auto-update | Manual config |
| Multi-chain | Config-based | Manual setup |
| Prototyping speed | Very fast | Moderate |
| Production flexibility | Limited | Full control |
Key Difference: Transaction Confirmation
// Scaffold-ETH: writeContractAsync waits for confirmation
await writeContractAsync({ functionName: "setGreeting", args: ["Hello"] });
// Transaction is CONFIRMED here
// Raw wagmi: need separate hook for confirmation
const { writeContract, data: hash } = useWriteContract();
const { isSuccess } = useWaitForTransactionReceipt({ hash });
// Must track isSuccess separately
Configuration
// packages/nextjs/scaffold.config.ts
import { defineConfig } from "~~/utils/scaffold-eth/defineConfig";
export default defineConfig({
targetNetworks: [chains.foundry], // Local dev
// targetNetworks: [chains.base], // Production
pollingInterval: 30000,
onlyLocalBurnerWallet: true, // Dev only
walletAutoConnect: true,
});
Adding a New Contract
- Create contract in
packages/foundry/contracts/ - Add to deploy script in
packages/foundry/script/Deploy.s.sol - Run
yarn deploy - Contract automatically appears in debug page and hooks
// packages/foundry/contracts/Vault.sol
pragma solidity ^0.8.20;
contract Vault {
mapping(address => uint256) public balances;
event Deposited(address indexed user, uint256 amount);
function deposit() external payable {
balances[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient");
balances[msg.sender] -= amount;
(bool ok,) = msg.sender.call{value: amount}("");
require(ok, "Transfer failed");
}
}
Production Deployment
# Build frontend
yarn next:build
# Deploy contracts to production chain
yarn deploy --network base
# The frontend reads from the generated contract data
# Deploy frontend to Vercel, IPFS, etc.
When to Use Scaffold-ETH
- Use for: Prototyping, hackathons, learning, MVPs, internal tools
- Consider raw wagmi for: Production dApps needing full customization, complex multi-contract UIs, or specific UX patterns
- Migration path: Start with Scaffold-ETH, extract components to raw wagmi as needed