DApp Development
Build a Web3 DApp with React and Ethereum
A practical architecture guide for connecting a React interface to Ethereum, reading contract state, sending transactions, and designing dependable wallet-aware UI.
Ketan Kachariya 5 min read
On This Page
A decentralized application still needs the disciplines of a good web application: clear state, accessible feedback, reliable data boundaries, and predictable failure handling. The difference is that part of its business logic and shared state lives on a blockchain. A browser wallet authorizes the user, an RPC connection carries requests to the network, and a smart contract defines the operations that can be executed. React is responsible for presenting that system without hiding important facts such as the active account, connected chain, transaction status, or the difference between a submitted transaction and a confirmed one.
How a DApp differs from a traditional application
A traditional frontend usually trusts an application server to authenticate the user, apply business rules, and write to a database. A DApp may still use an API and database, but a smart contract becomes an additional source of truth for the state that must be publicly verifiable. The frontend does not call the contract directly in a magical sense. It sends JSON-RPC requests through a provider. Read operations can use a public RPC endpoint, while write operations require a signer supplied by a wallet. That distinction should be visible in the architecture and in the code.
- React renders views, coordinates local state, and explains wallet and transaction status.
- A wallet provider exposes approved accounts and asks the user to sign messages or transactions.
- Ethers.js converts application calls into JSON-RPC requests and encodes contract operations using an ABI.
- The smart contract validates the request and changes on-chain state only when a transaction succeeds.
- Optional APIs or indexers prepare historical events and off-chain data for efficient queries.
Design the React boundary deliberately
Keep wallet state separate from product state. The connected address, chain ID, provider, signer, and connection status form one boundary. Contract data, pending mutations, and domain-specific errors form another. This separation prevents every component from reaching into window.ethereum and makes the interface easier to test. A small wallet context or hook can expose connection state, while focused data hooks read a balance or submit a particular contract action. Avoid storing a signer indefinitely without responding to account and network changes; recreate dependent objects when the wallet context changes.
Connect with Ethers.js v6
Ethers.js v6 uses BrowserProvider for an injected EIP-1193 wallet provider. Request account access only after a clear user action. Once access is granted, obtain a signer and create a Contract with a verified address and ABI. The example below reads a value and then submits an update. It is intentionally small: a production component should also check the chain, validate the input, decode expected errors, and prevent duplicate submissions.
import { BrowserProvider, Contract } from "ethers";
const counterAbi = [
"function value() view returns (uint256)",
"function setValue(uint256 nextValue)",
];
export async function updateCounter(address: string, nextValue: bigint) {
if (!window.ethereum) throw new Error("No supported browser wallet found");
const provider = new BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = await provider.getSigner();
const contract = new Contract(address, counterAbi, signer);
const currentValue: bigint = await contract.value();
const transaction = await contract.setValue(nextValue);
const receipt = await transaction.wait();
if (!receipt || receipt.status !== 1) {
throw new Error("The transaction did not complete successfully");
}
return { currentValue, transactionHash: receipt.hash };
}Treat reads and transactions as different experiences
A read can often be retried quietly and does not require user approval. A transaction may open a wallet prompt, remain pending, be replaced, or revert after it is mined. Model those stages explicitly: idle, awaiting signature, submitted, confirming, confirmed, and failed. Show the transaction hash as soon as it exists, but never label the action complete until the required confirmation is observed. If the wallet rejects the request, preserve the user's form input and explain that no transaction was sent. If the contract reverts, surface a safe, understandable message and retain diagnostic detail for logs.
Loading and error states are part of the product
Disable only the control that is actively submitting, not the whole interface. Distinguish a missing wallet from a disconnected wallet and a wrong network. Stale on-chain reads should be refetched after confirmation or reconciled from emitted events. Error copy should tell the user what they can do next: switch networks, reconnect, retry the RPC request, adjust an input, or inspect the transaction. Raw provider errors are useful for developers but should not be the only explanation a visitor receives.
Validate the network before every important action
Contract addresses are chain-specific. A valid address on one network can point to unrelated code or no code on another. Store supported chain IDs and contract addresses in typed configuration, compare them with provider.getNetwork(), and stop before creating a write request when they do not match. Listen for network changes and invalidate dependent reads. If the product offers a switch-network action, handle the case where the target chain has not been added to the wallet and explain the requested change before triggering another wallet prompt.
Deploy the whole system, not only the frontend
A DApp release should pin the correct ABI and contract address for each environment, protect server-side RPC credentials, and keep public configuration separate from secrets. Test with the production build, a supported wallet, realistic balances, rejected signatures, reverted calls, and a slow RPC connection. Contract deployment and website deployment are distinct releases: publishing the React bundle does not deploy Solidity code, and deploying a contract does not update the frontend configuration. Record both versions so a reported transaction can be traced to the interface and contract that produced it.
A dependable DApp makes blockchain state visible without making the user decode the blockchain themselves.
A practical path forward
Begin with one read and one write flow. Define the supported chain, verify the deployed address, and implement wallet states before expanding the feature set. Add event-driven refreshes only after the basic confirmation path works. This sequence produces a DApp that is easier to reason about because the UI, provider, signer, contract, and optional backend each have a clear responsibility. The technology is decentralized; the user experience still needs deliberate engineering.
Building a Web3 Product?
Need help with a DApp, smart contract integration, wallet connection, or production Web3 architecture? Let's discuss the engineering behind your product.
Discuss Your Web3 ProjectKeep Reading
Related Articles
Wallet Integration
Connect MetaMask to React for a Web3 Application
Build a safe wallet connection flow in React with account access, chain detection, event handling, rejection states, and clear security boundaries.
DApp Development
Smart Contract Integration with React
Connect a React frontend to a deployed contract with verified ABIs and addresses, reliable reads, transaction states, chain switching, and testable adapters.
Web3 Development
Ethers.js v6 Smart Contract Integration
Use Ethers.js v6 providers, signers, ABIs, contract reads, transaction writes, confirmations, and chain checks without mixing v5 patterns.