@web3-blocks/dapp-ui
v1.0.3
Published
The Universal and TypeScript-first Web3 abstraction layer that provides a unified provider, modular network hooks, and chain-specific utilities for building decentralized applications.
Maintainers
Readme
@web3-blocks/dapp-ui
The Universal and TypeScript-first Web3 abstraction layer that provides a unified provider, modular network hooks, and chain-specific utilities for building decentralized applications. It powers the logic of dApp/ui, the component library built on top of shadcn/ui — but can be used independently.
Installation
- Install with your package manager of choice
npm add @web3-blocks/dapp-uiPeer Dependencies
Install required peers that your app must provide. The SDK treats these as externals to prevent duplicate bundles and version conflicts:
react: ^18 || ^19react-dom: ^18 || ^19viem: ^2wagmi: ^2ethers: ^6@tanstack/react-query: ^4 || ^5
Example:
npm add react react-dom viem wagmi ethers @tanstack/react-query @web3-blocks/dapp-uiReact 19 Compatibility (Suppressing Peer Warnings)
Some transitive dependencies of wagmi may emit a peer warning for React 19 (via use-sync-external-store). To silence the warning without affecting functionality, add an override/resolution in your app:
- pnpm (
package.json):
{
"pnpm": {
"overrides": {
"use-sync-external-store": "^1.6.0"
}
}
}- npm (
package.json):
{
"overrides": {
"use-sync-external-store": "^1.6.0"
}
}- yarn (
package.json):
{
"resolutions": {
"use-sync-external-store": "^1.6.0"
}
}This does not change runtime behavior; it only removes the install-time warning when using React 19.
Quick Start
Minimal React/Next.js setup for Ethereum (EVM) with built-in provider:
import React from "react";
import { DAppUiProvider, useEth, Chains } from "@web3-blocks/dapp-ui";
function App() {
const { connect, disconnect, account, switchChain, chainId } = useEth();
const { address, isConnected } = account;
const { connectSafe, isPending } = connect;
return (
<div>
<p>Connected: {isConnected ? address : "-"}</p>
<p>Chain ID: {chainId ?? "?"}</p>
<button
disabled={isPending}
onClick={() =>
isConnected
? disconnect.disconnect()
: connectSafe({ connector: connect.injected })
}
>
{isPending ? "Connecting..." : isConnected ? "Disconnect" : "Connect"}
</button>
<div>
<p>Available chains:</p>
{switchChain.chains?.map((c) => (
<button
key={c.id}
onClick={() => switchChain.switchChain({ chainId: c.id })}
disabled={switchChain.isPending}
>
{c.name}
</button>
))}
</div>
</div>
);
}
export default function Root() {
return (
<DAppUiProvider
network="ethereum"
contract={{
address: "0x0000000000000000000000000000000000000000",
abi: [],
chains: [Chains.optimismSepolia],
defaultChain: Chains.optimismSepolia,
// Optional: rpcUrl for single-chain setups. Multi-chain uses default transports.
}}
>
<App />
</DAppUiProvider>
);
}Transactions with Lifecycle Callbacks
const { contract } = useEth();
async function addTask(content: string) {
await contract
.writeFn("addTask", [content], {
onSwitching: (msg) => console.log(msg),
onSwitched: (msg) => console.log(msg),
onSubmitted: (hash) => console.log("Tx submitted:", hash),
onConfirmed: (receipt) =>
console.log("Confirmed:", receipt.transactionHash),
})
.catch((err) => console.error("Transaction error:", err.message));
}Subscribe to Contract Events
const { contract } = useEth();
useEffect(() => {
const offAdded = contract.eventFn("TaskAdded", (user, id, content) => {
console.log("TaskAdded", { user, id: Number(id), content });
});
const offToggled = contract.eventFn("TaskToggled", (user, id, completed) => {
console.log("TaskToggled", {
user,
id: Number(id),
completed: Boolean(completed),
});
});
return () => {
offAdded?.();
offToggled?.();
};
}, [contract]);Exports
- Provider:
DAppUiProvider - Context:
useDAppContext - Types:
DAppUiProps,NETWORK_TYPES,DAppUiContextType - Ethereum (EVM):
useEth(combined convenience hook exposingaccount,connect,disconnect,contract,switchChain,chainId)- Individual hooks:
useConnect,useDisconnect,useAccount - Network hooks:
useSwitchChain(Wagmi) - Chains:
Chains(fromviem/chains) - Config:
createEthereumConfig(internal provider usage; returns Wagmi config if you need it externally)
Hooks mirror wagmi behavior. useConnect includes a convenience flag isWalletAvailable in addition to wagmi’s return.
Contributing
This project is open-source and welcomes contributions. The system is type-first and relies on a small contract config per network. Below is the complete flow, detailed to help you succeed.
Adding Networks
- Create
src/networks/<network>/config/contract.config.ts(use lowercase for<network>, e.g.,ethereum,sui). - Define and export
type ContractConfigin that file. - In
src/networks/<network>/index.ts, type re-export the contract config:
// src/networks/<network>/index.ts
export type { ContractConfig } from "./config/contract.config";Example (EVM):
// src/networks/ethereum/config/contract.config.ts
import type { Chain, Abi } from "viem";
export type ContractConfig = {
address: string; // EVM address for your contract
abi: Abi; // ABI describing callable functions and events
chains?: Chain[]; // Optional: supported chains (e.g., mainnet, sepolia)
};// src/networks/ethereum/index.ts
export type { ContractConfig } from "./config/contract.config";Type Generation (What Happens and Why)
- Run the generator:
npm run genotype- Don't ask me why I called is
genotype😑 - The generator scans
src/networks, validates folder names and ensures each network exportstype ContractConfig. - The generator scans
src/networks, validates folder names and ensures each network’sindex.tsexportstype ContractConfig(either a direct alias or a type re-export). - It then regenerates
src/constants/index.ts, which contains:NETWORK_TYPES: a union of all network keys (e.g.,"ethereum" | "sui").NETWORK_TYPES_ARRAY: a readonly array form, useful for iteration and validation.REGISTRIES: dynamic import map so the app can lazy-load network modules (code-splitting).NETWORK_CONTRACT_MAP: compile-time mapping from network → itsContractConfigtype.DAppUiPropsandDAppUiContextType: canonical provider and context types tied to the selected network.
Why dynamic imports? It avoids bundling every network’s code and lets consumers load only what they need.
Validation & Hooks
- A pre-commit hook runs the generator. If
src/constants/index.tschanges and isn’t staged, the commit fails with instructions. - The generator enforces:
- Lowercase-only folder names.
- Presence of
export type ContractConfigin each network’sindex.ts(alias or type re-export).
Troubleshooting
- Missing
index.tsor missingContractConfigexport → add the file and export the type exactly as shown. - Invalid folder name (uppercase or symbols) → rename the folder to lowercase letters only.
- TypeScript errors mentioning
NETWORK_CONTRACT_MAP[NETWORK_TYPES]→ ensure yourContractConfigmatches the fields used by your provider and hooks.
Network Switching & Chain ID
- Ensure your
chainsinclude all networks you want to support (e.g., Optimism Sepolia and Arbitrum). If only one chain is configured withrpcUrl, the provider uses it; multi-chain setups use default transports per chain. chainIdcomes from Wagmi and updates when MetaMask switches networks. Connect the wallet to reflect connector chain changes.
Transactions Failing with “Failed to fetch” (-32603)
- This usually indicates the wallet’s RPC endpoint is unreachable or blocked (ad-blockers or corporate proxies). Try disabling blockers on localhost and confirm MetaMask is connected to the intended chain.
- If the contract call reverts without a reason or the contract isn’t deployed on the active chain,
writeFnthrows a clear message instead of a rawCALL_EXCEPTION.
Dependency Management (SDK Consumers)
The SDK externalizes peers (
react,react-dom,wagmi,viem,ethers,@tanstack/react-query) to avoid duplicate installations and reduce bundle bloat.Use consistent version ranges in your app. Recommended:
react,react-dom:^18 || ^19wagmi:^2viem:^2ethers:^6@tanstack/react-query:^4 || ^5
If you are missing a required peer, install it explicitly. The library does not bundle peers by design.
If you encounter peer version warnings, set
overrides/resolutions:- pnpm (
package.json):
{ "pnpm": { "overrides": { "use-sync-external-store": "^1.6.0", "react": "^19", "react-dom": "^19" } } }- npm (
package.json):
{ "overrides": { "use-sync-external-store": "^1.6.0", "react": "^19", "react-dom": "^19" } }- yarn (
package.json):
{ "resolutions": { "use-sync-external-store": "^1.6.0", "react": "^19", "react-dom": "^19" } }- pnpm (
License
- MIT © dApp/ui
- React 19 peer warning:
use-sync-external-store
If you see a peer warning like:
✕ unmet peer react@"^16.8.0 || ^17.0.0 || ^18.0.0": found 19.xThis comes from a transitive dependency chain (wagmi → WalletConnect stack → valtio → [email protected]) that only declares React up to 18.
To silence the warning and stay compatible with React 19, override use-sync-external-store to a React-19-compatible release (≥ 1.6.0):
- npm (
package.json):
{
"overrides": {
"use-sync-external-store": "^1.6.0"
}
}- pnpm (
package.json):
{
"pnpm": {
"overrides": {
"use-sync-external-store": "^1.6.0"
}
}
}- yarn (
package.json):
{
"resolutions": {
"use-sync-external-store": "^1.6.0"
}
}Alternatively (Yarn Berry), extend the peer range without changing versions:
packageExtensions:
use-sync-external-store@*:
peerDependencies:
react: ">=16.8.0 <21"