Build a full stack dApp using React.js

Sreekar Reddy
Simform Engineering
11 min readJun 13, 2023

Building bridges to the future: A complete guide to developing a full stack dApp with React.js.

Decentralized applications (dApps) are distributed open-source applications that run on a blockchain network. Due to their decentralization, trustlessness, and verifiable behavior, they provide a lot of use cases and solutions in various sectors.

This article guides developers to build a full-stack decentralized application with Hardhat and React.js, covering everything from environment setup to deployment on the Ethereum blockchain.

What is Hardhat?

Hardhat is a development framework for building Ethereum applications consisting of various components for compiling, debugging, and deploying smart contracts.

Refer to this article for Smart contract development using Hardhat

Hardhat framework is used for writing and deploying smart contracts. We will use React.js for building frontend.

Your computer needs to have Node.js and npm installed.

Here are the steps we will follow to create the dApp:

  1. Create a new React.js project: Open your hardhat project and create React app using the following command.
npx create-react-app my-app

This will create a new React.js project with the name “my-app”. You can choose the application name of your choice.

We can run the web server that came with the React app to ensure that everything works correctly.

npm run start

2. Install dependencies: Ethers.js is a popular library to interact with Ethereum smart contracts that help us sign transactions. Install it using the following command:

npm install ethers

3. Configure metamask: Metamask is a browser extension that allows users to interact with Ethereum dApps. You can configure Metamask by installing it on your browser and creating a new account (installation guide).

Open the directory using VS code or any other code editor of your choice.

Let’s look at our smart contract again.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract IncrementDecrement{
int256 public counter;

function increment() public {
counter++;
}

function decrement() public {
counter--;
}

function reset() public {
counter = 0;
}
}

Add another getter function and events in the contract. Replace the file content with the following:

pragma solidity ^0.8.7;

contract IncrementDecrement {
// State variables are stored on the blockchain.
uint256 value;

event Increment(string message);
event Decrement(string message);

function increment() external {
value++;

emit Increment("value incremented by 1");
}

function decrement() external {
value--;

emit Decrement("value decremented by 1");
}

// getter function
function getValue() public view returns (uint256) {
return value;
}
}

The contract is named “IncrementDecrement”, and it contains a single state variable, “value”, which is of the uint256 data type. State variables in smart contracts are stored on the blockchain, allowing their values to be accessed and modified by the contract’s functions.

“Increment” and “Decrement” events are emitted when the counter value increases or decreases. They are a way to store data on the transaction log.

Additionally, the contract has two external functions: “increment” and “decrement”. These functions are responsible for increasing and decreasing the value of the “value” state variable by 1, respectively. Each function emits an event indicating that the value has been modified.

Finally, the contract defines a public function called “getValue” that allows external applications to read the value of the “value” state variable. This function returns the current value of the “value” state variable as a uint256 data type.

Now, you have to deploy it on any testnet.

Testnets are simulations of the mainnet that offer the same functionalities as a blockchain. They are a safer way for developers to test their applications before deploying them on the main network.

Let’s look at the hardhat.config.js file. We need to add the path where our artifacts will be stored. We need to set the path inside the React folder such that it has access to the contract abi.

You can add a paths section as shown below:

require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.18",
paths: {
artifacts: './src/artifacts'
},
networks: {
hardhat: {
chainId: 1337,
}
}
};

Hardhat comes built-in with a special network called hardhat. When using this network, an instance of the Hardhat Network will be automatically created when you run a task, script, or test your smart contracts.

paths is the section where all of the artifacts will be stored.

Let’s now compile our smart contracts using the following command.

npx hardhat compile

Once the compilation is finished, you should be able to see the artifacts folder inside the src folder.

Open up a new terminal and run the deployment script. Save the contract address.

Once the contract is deployed, we can start integrating it with frontend.

Interacting with Smart Contract

To integrate our app (front-end) with Smart Contract, we need two pieces of information.

a. The Contract Address — is like a unique identifier for your smart contract on the blockchain. It serves as a location where your smart contract is deployed and can be interacted with by users.

b. The Contract ABI — is a JSON description of how the smart contract behaves. It describes its functions, behavior, etc.

We need to import smart contracts into the App component. So, go to src/App.js, replace the file, and add import statements.

import { useState } from "react";
import { ethers } from "ethers";
import IncrementDecrement from "./artifacts/contracts/IncrementDecrement.sol/IncrementDecrement.json";

With those three import statements, we ensure that the Ethers.js library is imported, the useState React Hook is made available, and the contract ABI is imported.

Ethers.js: Ethers.js provides a Web3 Provider which can be used to connect to the Ethereum network. This Provider can be used to send transactions, interact with smart contracts, and retrieve data from the Ethereum network.

Add a variable that holds your contract address.

const contractAddress = "YOUR_CONTRACT_ADDRESS"; // Replace with the actual contract address

Create a simple app component in which we will add further implementation later.

function App() {
const [value, setValue] = useState();
const [address, setAddress] = useState('');

const connectToMetaMask = () => {}
const IncrementHandler = () => {}
const DecrementHandler = () => {}
const ReadContractValue = () => {}
const ConvertValue = () => {}

return (
<div className="App">
<div className="connectBtns">
<button className="btn" onClick={connectToMetaMask}>
Connect To MetaMask
</button>
</div>

<div className="display">
<p className="key">
Address: <span className="value">{address}</span>
</p>

<div className="valueContainer">
<p className="key">
Value: <span>{value ?? ''}</span>
</p>

<button onClick={ConvertValue} className="btn" disabled={!value}>
deCode
</button>
</div>
</div>

<div className="actionBtns">
<button
className="btn minus"
onClick={DecrementHandler}
title="decrement"
>
-
</button>

<button
className="btn plus"
onClick={IncrementHandler}
title="increment"
>
+
</button>
<button className="btn" onClick={ReadContractValue} title="read value">
get value
</button>
</div>
</div>
);
}

useState hook is used to track the function component state. We will use useState hook to update the counter value.

To connect the application with the wallet, we will create a function called connectToMetaMask . Additional functions are created to interact with the counter value, such as:

a. IncrementHandler

b. DecrementHandler

c. ReadContractValue

ConnectToMetamask function

// connectToMetaMask function
const ConnectToMetaMask = async () => {
try {
// Check if MetaMask is installed
if (window.ethereum) {
// Request account access
const Accounts = await window.ethereum.request({
method: 'eth_requestAccounts',
});

setAddress(Accounts[0]);
console.log('Connected to MetaMask!', Accounts);
} else {
console.error(
'MetaMask not found. Please install MetaMask to use this application.',
);
}
} catch (error) {
console.error(error);
}
}

The connectToMetaMaskfunction is used to connect the user's MetaMask wallet to the application. It starts by checking if the window.ethereum object exists, which indicates that MetaMask is installed in the user's browser.

If window.ethereum exists, the function requests account access by calling the window.ethereum.request method with the method parameter set to 'eth_requestAccounts'. This method prompts the user to connect their MetaMask wallet to the application and returns an array of the user's Ethereum addresses. The first address is set as the current address.

If window.ethereum does not exist, the function logs an error message to the console and informs the user that they must install MetaMask to use the application.

The “Connect To MetaMask” button should give a metamask wallet pop-up, as shown in the picture.

DecrementHandler function

const DecrementHandler = async () => {
try {
if (window.ethereum) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const Signer = provider.getSigner();

const Contract = new ethers.Contract(contractAddress, IncrementDecrement.abi, Signer);

const Tx = await Contract.decrement();
const TxRecit = await Tx.wait();
console.log('after :', TxRecit);
} else {
console.error(
'MetaMask not found. Please install MetaMask to use this application.',
);
}
} catch (error) {
console.log(error);
alert(error);
}
};

This function, DecrementHandler, is an asynchronous function that handles a "decrement" operation on a smart contract using Ether.js and MetaMask.

The function first checks if window.ethereum exists (which indicates that the MetaMask extension is installed and enabled). If it does, use them ethers.providers.Web3Provider to create a provider object connected to the user's MetaMask wallet.

The function then creates an Signer object using provider.getSigner(), which is used to sign transactions from the user's Ethereum account. It also creates a new ethers.Contract object with ContractAddress and ABI parameters that are passed in. This object represents the smart contract that the function will interact with.

Next, decrement function is called internally, which reduces the counter value.

If window.ethereum does not exist, an error message will be logged the user will be notified. If an error occurs at any point during the execution of the function, it is caught and logged to the console, and an alert is shown to the user with the error message.

Similar to above DecrementHandler function, we haveIncrementHandler, and ReadContractValue function.

The complete App.js will look like this:

import { useState } from "react";
import { ethers } from "ethers";
import IncrementDecrement from "./artifacts/contracts/IncrementDecrement.sol/IncrementDecrement.json";
import "./App.css"

const contractAddress = "YOUR_CONTRACT_ADDRESS"; // Replace with the actual contract address

function App() {
const [value, setValue] = useState();
const [address, setAddress] = useState('');

useEffect(() => {
if (window.ethereum && window.ethereum.selectedAddress) {
// MetaMask is connected
const selectedAddress = window.ethereum.selectedAddress;
console.log(`Connected to MetaMask with address: ${selectedAddress}`);
} else {
// MetaMask is not connected
console.log('MetaMask is not connected');
}
}, []);

async function connectToMetaMask() {
try {
// Check if MetaMask is installed
if (window.ethereum) {
// Request account access
const Accounts = await window.ethereum.request({
method: 'eth_requestAccounts',
});

setAddress(Accounts[0]);
console.log('Connected to MetaMask!', Accounts);
} else {
console.error(
'MetaMask not found. Please install MetaMask to use this application.',
);
}
} catch (error) {
console.error(error);
}
}

async function disconnectFromMetaMask() {
try {
// Check if MetaMask is installed
if (window.ethereum) {
// Disconnect from MetaMask
await window.ethereum.request({
method: 'wallet_requestPermissions',
params: [{ eth_accounts: {} }],
});
console.log('Disconnected from MetaMask!');
} else {
console.error(
'MetaMask not found. Please install MetaMask to use this application.',
);
}
} catch (error) {
console.error(error);
}
}

const DecrementHandler = async () => {
try {
if (window.ethereum) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const Signer = provider.getSigner();

const Contract = new ethers.Contract(contractAddress, IncrementDecrement.abi, Signer);

const Tx = await Contract.decrement();
const TxRecit = await Tx.wait();
console.log('after :', TxRecit);
} else {
console.error(
'MetaMask not found. Please install MetaMask to use this application.',
);
}
} catch (error) {
console.log(error);
alert(error);
}
};

const IncrementHandler = async () => {
try {
if (window.ethereum) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const Signer = provider.getSigner();

const Contract = new ethers.Contract(contractAddress, IncrementDecrement.abi, Signer);

const Tx = await Contract.increment();
const TxRecit = await Tx.wait();
console.log('after :', TxRecit);
} else {
console.error(
'MetaMask not found. Please install MetaMask to use this application.',
);
}
} catch (error) {
console.log(error);
alert(error);
}
};

const ReadContractValue = async () => {
try {
if (window.ethereum) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const Signer = provider.getSigner();

// Create a new instance of the Contract class
const Contract = new ethers.Contract(contractAddress, IncrementDecrement.abi, Signer);

// Call the getValue function from the contract
const Tx = await Contract.getValue();
console.log('Tx :', Tx);

setValue(Tx._hex);
} else {
console.error(
'MetaMask not found. Please install MetaMask to use this application.',
);
}
} catch (error) {
console.log(error);
alert(error);
}
};

const ConvertValue = () => {
const temp = ethers.BigNumber.from(value).toNumber();
setValue(temp);
};

return (
<div className="App">
<div className="connectBtns">
<button className="btn" onClick={connectToMetaMask}>
Connect To MetaMask
</button>
</div>

<div className="display">
<p className="key">
Address: <span className="value">{address}</span>
</p>

<div className="valueContainer">
<p className="key">
Value: <span>{value ?? ''}</span>
</p>

<button onClick={ConvertValue} className="btn" disabled={!value}>
deCode
</button>
</div>
</div>

<div className="actionBtns">
<button
className="btn minus"
onClick={DecrementHandler}
title="decrement"
>
-
</button>

<button
className="btn plus"
onClick={IncrementHandler}
title="increment"
>
+
</button>
<button className="btn" onClick={ReadContractValue} title="read value">
get value
</button>
</div>
</div>
);
}

export default App;

Add styling to the page from the CSS file.
Go into the src/App.css file and replace it with the following code.

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

.App {

margin: auto;
min-height: 100vh;
text-align: center;

display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

.connectBtns,
.actionBtns {
display: flex;
align-items: center;
justify-content: center;
}
.connectBtns {
gap: 20px;
}

.actionBtns {
gap: 30px;
}
.actionBtns > .btn {
padding: 10px 30px;
}

.btn {
cursor: pointer;
border-radius: 10px;
padding: 10px 20px;
border: none;
font-size: 16px;
text-align: center;
text-transform: uppercase;
transition: 0.6s;
border-radius: 10px;
display: block;
background-size: 200% auto;
}


} */
.btn:disabled {
cursor: not-allowed;
opacity: 50%;
}

.display {
border: 1px solid #2b8a3e;
border-radius: 10px;
min-height: 200px;
min-width: 500px;
margin: 20px auto;
padding: 40px 20px;
text-align: left;
}

.valueContainer {
margin-top: 32px;
display: flex;
justify-content: space-between;
}

.key {
font-size: 20px;
}

.value {
font-size: 16px;
}
.minus,
.plus {
font-size: 20px;
}

On the metamask, make sure you are on the same network as you deployed.

If you deployed on the hardhat local blockchain, go to the local host network on metamask and ensure it is connected to the same port as the local blockchain on a hardhat. Import the private key of any accounts from the local blockchain in hardhat into metamask accounts.

Now, you can start the React app by running the following command.

npm run start

Your app should look like this.

Click on the plus or minus symbol to interact with the application.

You should see the counter value change as you confirm the transaction. The value is in encoded form. To decode the value into a number, click on the DECODE button.

That is it! You have your dApp on Blockchain.

Conclusion

Integrating smart contracts into your React.js web app offers substantial advantages that can elevate your project to new horizons. By following the outlined steps and leveraging the power of smart contracts, you can unlock a myriad of opportunities, enhance the efficiency and security of your transactions, and drive innovation within your industry.

Happy coding :)

Meanwhile, follow Simform Engineering for more such valuable information.

--

--