🛠️ Setting Up Your Frontend dApp with thirdweb for Rootstock
A complete guide to building a frontend dApp using thirdweb SDKs, configured for Rootstock’s testnet or mainnet, ready to integrate with your contract

I love crafting beautiful web experiences and writing efficient smart contracts. I'm currently exploring Zero-Knowledge proofs, as well as Functional, Systems, Concurrent, and Scripting Programming Languages (Rust, Erlang, and Python).
💪 Prerequisites
Before starting, make sure you have the following:
Node.js (v16 or higher)
Package Manager: npm, yarn, or pnpm
MetaMask or any EVM-compatible wallet
Thirdweb Client ID: Get this from the thirdweb dashboard
Deployed smart contract on Rootstock testnet (chain ID 31) or mainnet (chain ID 30)
📦 Step 1: Bootstrap Your Frontend with thirdweb (Part A - If you’re bootstrapping a new project from scratch)
Start by generating a project scaffold using thirdweb CLI:
npx thirdweb create
Follow the CLI prompts:
Project: App
Project name: thirdweb-app
Framework: Next.js

This will initialize your thirdweb-app and start to install dependencies

Then, open up your app in VS Code and run your server
cd thirdweb-app
code .
yarn dev
Part B - If you want to add thirdweb to your existing Nextjs app)
In your terminal, install thirdweb by running the command
npm install thirdweb
Then create a client.ts file and paste in the following code
import { createThirdwebClient } from "thirdweb";
// Replace this with your client ID string
// refer to https://portal.thirdweb.com/typescript/v5/client on how to get a client ID
const clientId = process.env.NEXT_PUBLIC_TEMPLATE_CLIENT_ID;
if (!clientId) {
throw new Error("No client ID provided");
}
export const client = createThirdwebClient({
clientId: clientId,
});
At the root of your application, wrap your app with a <ThirdwebProvider/> component. This keeps the state around like the active wallet and chain.
import "./globals.css";
import { ThirdwebProvider } from "thirdweb/react";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<ThirdwebProvider>{children}</ThirdwebProvider>
</body>
</html>
);
}
This guide is for setting up thirdweb for a Next.js project. To see setting up thirdweb for React (vite) projects, check here
🌐 Step 2: Define Rootstock Network Support
Create a new file chains/rootstock.ts and define Rootstock's mainnet and testnet configuration:
// chains/rootstock.ts
import { defineChain } from "thirdweb";
export const rootstockMainnet = defineChain({
id: 30,
name: "Rootstock Mainnet",
rpc: "https://30.rpc.thirdweb.com",
nativeCurrency: {
name: "RBTC",
symbol: "RBTC",
decimals: 18,
},
blockExplorers: [
{ name: "RSK Explorer", url: "https://explorer.rsk.co" },
{ name: "Blockscout", url: "https://rootstock.blockscout.com" },
]
});
export const rootstockTestnet = defineChain({
id: 31,
name: "Rootstock Testnet",
rpc: "https://public-node.testnet.rsk.co",
nativeCurrency: {
name: "tRBTC",
symbol: "tRBTC",
decimals: 18,
},
blockExplorers: [
{ name: "RSK Testnet Explorer", url: "https://explorer.testnet.rootstock.io/" },
{ name: "Blockscout Testnet Explorer", url: "https://rootstock-testnet.blockscout.com/" },
],
testnet: true,
});
📝 Step 3: Configure the thirdweb
Add your clientId to an .env file; if you’ve still not gotten your client ID, you can get it here
NEXT_PUBLIC_THIRDWEB_CLIENT_ID=your_client_id
Now you can run your server by running
yarn dev
You should see something like this below

🔌 Step 4: Configure Connect Button for Rootstock Testnet
In your page.tsx, import your rootstockTestnet and add it as a chain to your <ConnectButton/>
"use client";
import Image from "next/image";
import { ConnectButton } from "thirdweb/react";
import thirdwebIcon from "@public/thirdweb.svg";
import { client } from "./client";
import { rootstockTestnet } from "./chains/rootstock"; //importing rooststockTestnet from ./chains/rootstock
export default function Home() {
return (
<main className="p-4 pb-10 min-h-[100vh] flex items-center justify-center container max-w-screen-lg mx-auto">
<div className="py-20">
<Header />
<div className="flex justify-center mb-20">
<ConnectButton
client={client}
chain={rootstockTestnet} // we added rootstockTestnet as our default chain here
/>
</div>
// rest of your code
</div>
</main>
);
}
// rest of your code
The ConnectButton the component is themeable and supports 500+ wallets. Read here for more settings to configure the theme.
Now, head to your dApp and connect your wallet, and see how it goes





And that’s it, you have successfully integrated/set up thirdweb with your Next.js App. Now let’s see how we can do simple read and write functions to the blockchain using Thirdweb. For that, I’ve deployed a SimpleStorage contract
It has a set function, a write function, a get function, a read function, which will be the read and write functions we’ll explore with this contract. Here’s the contract address 0x53b4fF9D9A424971539cdb96Cabc95c19eDaaFfA You can view it here on the rootstock testnet explorer
🧠 Step 5: Integrate Your Smart Contract
First, we access the currently connected account to the dApp, You can do that by using the useActiveAccount hook from thirdweb in your page.tsx
const account = useActiveAccount();
And then, based on this connected account, we can conditionally display the input field to update the stored string in the contract, and also display the current stored string in the contract.
{account && (
<div className="flex flex-col gap-4 mb-20">
<h3 className="text-2xl font-medium mb-2">
Stored String: {isLoading || isRefetching ? "fetching..." : storedString}
</h3>
<button
onClick={() => refetch()}
className="mb-4 px-4 py-2 border bg-gray-600 text-white rounded max-w-[140px] hover:bg-gray-700 transition-colors disabled:cursor-not-allowed bl"
disabled={isLoading || isRefetching}
>
{isRefetching ? "Refreshing" : "Refresh string" }
</button>
<div className="flex flex-col gap-4">
<span className="text-xl">Update stored string</span>
<input
type="text"
value={newString}
onChange={(e) => setNewString(e.target.value)}
className="border border-zinc-700 rounded outline-none px-4 py-2 bg-zinc-900 text-zinc-300"
placeholder="Enter new string"
/>
<button
onClick={() => handleSetStoredString(newString)}
className="px-4 py-2 max-w-[140px] bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors disabled:cursor-not-allowed"
disabled={updating || !newString}
>
{updating ? "Updating" : "Update"}
</button>
</div>
</div>
)}
Get Contract Instance
import { client } from "./client";
import { rootstockTestnet } from "./chains/rootstock";
import {
getContract
} from "thirdweb";
const contract = getContract({
address: "0x53b4fF9D9A424971539cdb96Cabc95c19eDaaFfA",
chain: rootstockTestnet,
client,
});
Reading Data
import {
useReadContract
} from "thirdweb/react";
const { data: storedString, isPending: isLoading, refetch, isRefetching } = useReadContract({
contract,
method: "function get() external view returns (string memory)",
params: [],
});
Writing Data
import {
prepareContractCall,
sendAndConfirmTransaction,
} from "thirdweb";
const handleSetStoredString = async (newString: string) => {
if (!account) return;
try {
setUpdating(true);
const setTx = prepareContractCall({
contract: contract,
method: "function set(string memory _newString) external",
params: [newString],
});
await sendAndConfirmTransaction({
transaction: setTx,
account,
});
setUpdating(false);
setNewString("");
alert("Update string on chain successul")
} catch (error) {
console.error();
alert("An error occured");
setUpdating(false);
}
};
Putting it all together.
"use client";
import Image from "next/image";
import {
ConnectButton,
useActiveAccount,
useReadContract,
} from "thirdweb/react";
import thirdwebIcon from "@public/thirdweb.svg";
import { client } from "./client";
import { rootstockTestnet } from "./chains/rootstock";
import {
getContract,
prepareContractCall,
sendAndConfirmTransaction,
} from "thirdweb";
import { useState } from "react";
export default function Home() {
const account = useActiveAccount();
const [newString, setNewString] = useState("");
const [updating, setUpdating] = useState(false);
const contract = getContract({
address: "0x53b4fF9D9A424971539cdb96Cabc95c19eDaaFfA",
chain: rootstockTestnet,
client,
});
const { data: storedString, isPending: isLoading } = useReadContract({
contract,
method: "function get() external view returns (string memory)",
params: [],
});
const handleSetStoredString = async (newString: string) => {
if (!account) return;
try {
setUpdating(true);
const setTx = prepareContractCall({
contract: contract,
method: "function set(string memory _newString) external",
params: [newString],
});
await sendAndConfirmTransaction({
transaction: setTx,
account,
});
setUpdating(false);
setNewString("");
alert("Update string on chain successul")
} catch (error) {
console.error();
alert("An error occured");
setUpdating(false);
}
};
return (
<main className="p-4 pb-10 min-h-[100vh] flex items-center justify-center container max-w-screen-lg mx-auto">
<div className="py-20">
<Header />
<div className="flex justify-center mb-16">
<ConnectButton client={client} chain={rootstockTestnet} />
</div>
{account && (
<div className="flex flex-col gap-4 mb-20">
<h3 className="text-2xl font-medium mb-2">
Stored String: {isLoading || isRefetching ? "fetching..." : storedString}
</h3>
<button
onClick={() => refetch()}
className="mb-4 px-4 py-2 border bg-gray-600 text-white rounded max-w-[140px] hover:bg-gray-700 transition-colors disabled:cursor-not-allowed bl"
disabled={isLoading || isRefetching}
>
{isRefetching ? "Refreshing" : "Refresh string" }
</button>
<div className="flex flex-col gap-4">
<span className="text-xl">Update stored string</span>
<input
type="text"
value={newString}
onChange={(e) => setNewString(e.target.value)}
className="border border-zinc-700 rounded outline-none px-4 py-2 bg-zinc-900 text-zinc-300"
placeholder="Enter new string"
/>
<button
onClick={() => handleSetStoredString(newString)}
className="px-4 py-2 max-w-[140px] bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors disabled:cursor-not-allowed"
disabled={updating || !newString}
>
{updating ? "Updating" : "Update"}
</button>
</div>
</div>
)}
// rest of your code
</div>
</main>
);
}
//rest of your code
Now, let’s test,
First, you should see something like this,

Enter your new string and fire the write transaction to update the string and confirm the transaction in your wallet

Updating the storedString in the contract was successful

Now we refresh

And now we have the updated string being displayed


🔹 Final Thoughts
In conclusion, setting up a frontend dApp with thirdweb for Rootstock is a streamlined process that empowers developers to quickly build secure and user-friendly applications. By leveraging thirdweb’s SDKs and the new Rootstock support, you can efficiently connect your frontend to deployed smart contracts with minimal boilerplate. Whether experimenting on testnet or going live on mainnet, this setup ensures you're ready to ship fast.
The full working code for this tutorial can be found here https://github.com/michojekunle/Nextjs-Thirdweb-RSK-Setup, I’ve also deployed it live here.
Be sure to consult the official Rootstock docs and thirdweb docs for more options and extensions! Join the Rootstock community on Discord. Happy Coding 🥳!





