Skip to main content

Simple Frontend

Building upon the Counter contract you interacted with in Step 1: Contract Interaction and deployed locally in Step 2: Local Development, this tutorial will guide you through creating a simple frontend application using Next.js to interact with the Counter smart contract on the Flow blockchain. Using the Flow Client Library (FCL), you'll learn how to read and modify the contract's state from a React web application and set up wallet authentication using FCL's Discovery UI.

Objectives

After completing this guide, you'll be able to:

  • Display data from a Cadence smart contract (Counter) on a Next.js frontend using the Flow Client Library.
  • Mutate the state of a smart contract by sending transactions using FCL and a wallet.
  • Set up the Discovery UI to use a wallet for authentication.

Creating the App

First, let's create our Next.js app and navigate to it with the following terminal commands. From the root of where you keep your source code:


_10
npx create-next-app@latest fcl-app-quickstart
_10
cd fcl-app-quickstart

This command sets up a new Next.js project named fcl-app-quickstart. Then, we navigate into the project directory.

Open the new project in your editor.

The default layout includes some boilerplate code that we don't need. Let's simplify pages/index.js to start with a clean slate. Replace the contents of pages/index.js with:


_10
// pages/index.js
_10
_10
export default function Home() {
_10
return (
_10
<div>
_10
<h1>FCL App Quickstart</h1>
_10
</div>
_10
);
_10
}

This code defines a simple Home component that displays the text "FCL App Quickstart".

Now, let's run our app with the following npm command:


_10
npm run dev

This will start the development server and open your app in the browser at http://localhost:3000. You will see a page displaying FCL App Quickstart.

Setting Up FCL

To interact with the Flow blockchain, we need to install the Flow Client Library (FCL). In your terminal, run the following command to install FCL:


_10
npm install @onflow/fcl

This command installs FCL and adds it to your project's dependencies.

Next, we'll configure FCL to connect to the Flow Testnet. An Access Node serves as the primary point of interaction for clients to communicate with the Flow network. It provides a gateway for submitting transactions, querying data, and retrieving information.

Create a new file flow/config.js (you may need to create the flow directory inside your project root) and add the following configuration code:


_10
// flow/config.js
_10
_10
import * as fcl from "@onflow/fcl";
_10
_10
fcl.config({
_10
"accessNode.api": "https://rest-testnet.onflow.org",
_10
"discovery.wallet": "https://fcl-discovery.onflow.org/testnet/authn",
_10
});

Then, in your pages/_app.js file, import the configuration:


_10
// pages/_app.js
_10
_10
import "../styles/globals.css";
_10
import "../flow/config"; // Import the FCL configuration
_10
_10
function MyApp({ Component, pageProps }) {
_10
return <Component {...pageProps} />;
_10
}
_10
_10
export default MyApp;

Querying the Chain

Now, let's read data from the Counter smart contract deployed on the Flow Testnet. We'll use the Counter contract deployed to the account 0x8a4dce54554b225d (the same contract we used in previous steps).

This contract has a public function getCount() that we can call to retrieve the current count.

First, we'll set up state in our app to store the count and manage component updates. We'll use React's useState and useEffect hooks.

Update your imports in pages/index.js to include useState, useEffect, and fcl:


_10
// pages/index.js
_10
_10
import { useState, useEffect } from "react";
_10
import * as fcl from "@onflow/fcl";

Next, initialize the count state variable inside your Home component:


_10
const [count, setCount] = useState(0);

Now, let's create a function to query the count from the blockchain:


_17
const queryCount = async () => {
_17
try {
_17
const res = await fcl.query({
_17
cadence: `
_17
import Counter from 0x8a4dce54554b225d
_17
_17
access(all)
_17
fun main(): Int {
_17
return Counter.getCount()
_17
}
_17
`,
_17
});
_17
setCount(res);
_17
} catch (error) {
_17
console.error("Error querying count:", error);
_17
}
_17
};

  • Explanation:
    • We use fcl.query to send a script to the Flow blockchain.
    • The Cadence script imports the Counter contract and defines a main function that returns the count variable via the getCount() function.
    • The result of the query is stored in res, and we update the count state with setCount(res).

Next, use the useEffect hook to call queryCount when the component mounts:


_10
useEffect(() => {
_10
queryCount();
_10
}, []);

The empty array [] ensures that queryCount is called only once when the component first renders.

Finally, update the return statement to display the count:


_10
return (
_10
<div>
_10
<h1>FCL App Quickstart</h1>
_10
<div>Count: {count}</div>
_10
</div>
_10
);

At this point, your pages/index.js file should look like this:


_37
// pages/index.js
_37
_37
import { useState, useEffect } from "react";
_37
import * as fcl from "@onflow/fcl";
_37
_37
export default function Home() {
_37
const [count, setCount] = useState(0);
_37
_37
const queryCount = async () => {
_37
try {
_37
const res = await fcl.query({
_37
cadence: `
_37
import Counter from 0x8a4dce54554b225d
_37
_37
access(all)
_37
fun main(): Int {
_37
return Counter.getCount()
_37
}
_37
`,
_37
});
_37
setCount(res);
_37
} catch (error) {
_37
console.error("Error querying count:", error);
_37
}
_37
};
_37
_37
useEffect(() => {
_37
queryCount();
_37
}, []);
_37
_37
return (
_37
<div>
_37
<h1>FCL App Quickstart</h1>
_37
<div>Count: {count}</div>
_37
</div>
_37
);
_37
}

Now, run npm run dev again. After a moment, the count from the Counter contract should appear on your page!

Mutating the Chain State

Now that we've successfully read data from the Flow blockchain, let's modify the state by incrementing the count in the Counter contract. To do this, we'll need to send a transaction to the blockchain, which requires user authentication through a wallet.

Creating a Flow Wallet and Funding Your Testnet Account

Before sending transactions, you need a Flow wallet to authenticate and interact with the blockchain. You can create a wallet by visiting the Flow Testnet Wallet website. Follow the on-screen instructions to set up your wallet.

Once you have a wallet, make sure that your account has enough FLOW tokens to cover transaction fees on the Flow Testnet. You can fund your testnet account using the Flow Testnet Faucet. Simply enter your account address and submit to receive testnet FLOW tokens.

Setting Up Wallet Authentication with Discovery

Before we can send a transaction, we need to set up wallet authentication. We'll use FCL's Discovery UI to allow users to connect their wallet with minimal setup.

Add the following line to your FCL configuration in src/App.js:


_10
fcl.config({
_10
'accessNode.api': 'https://rest-testnet.onflow.org',
_10
'discovery.wallet': 'https://fcl-discovery.onflow.org/testnet/authn',
_10
});

One-Line Discovery UI Setup:

  • By adding the 'discovery.wallet' line to the FCL configuration, we enable the Discovery UI.
  • This provides an interface for users to select and authenticate with a wallet.
  • This is all it takes—just one line—to integrate wallet selection into your app.

Adding Authentication Buttons

Let's add a simple authentication flow to our app. We'll allow users to log in and log out, and display their account address when they're logged in.

First, add new state variables to manage the user's authentication state:


_10
const [user, setUser] = useState({ loggedIn: false });

Then, use useEffect to subscribe to the current user's authentication state:


_10
useEffect(() => {
_10
fcl.currentUser.subscribe(setUser);
_10
queryCount();
_10
}, []);

  • Explanation:
    • fcl.currentUser.subscribe(setUser) sets up a listener that updates the user state whenever the authentication state changes.
    • We also call queryCount() to fetch the count when the component mounts.

Next, define the logIn and logOut functions:


_10
const logIn = () => {
_10
fcl.authenticate();
_10
};
_10
_10
const logOut = () => {
_10
fcl.unauthenticate();
_10
};

  • Explanation:
    • fcl.authenticate() triggers the authentication process using the Discovery UI.
    • fcl.unauthenticate() logs the user out.

Now, update the return statement to include authentication buttons and display the user's address when they're logged in:


_15
return (
_15
<div>
_15
<h1>FCL App Quickstart</h1>
_15
<div>Count: {count}</div>
_15
{user.loggedIn ? (
_15
<div>
_15
<p>Address: {user.addr}</p>
_15
<button onClick={logOut}>Log Out</button>
_15
{/* We'll add the transaction button here later */}
_15
</div>
_15
) : (
_15
<button onClick={logIn}>Log In</button>
_15
)}
_15
</div>
_15
);

Sending a Transaction to Increment the Counter

Next, we'll add a button to allow the user to increment the count by sending a transaction to the blockchain.

First, define the incrementCount function:


_29
const incrementCount = async () => {
_29
try {
_29
const transactionId = await fcl.mutate({
_29
cadence: `
_29
import Counter from 0x8a4dce54554b225d
_29
_29
transaction {
_29
prepare(acct: AuthAccount) {}
_29
execute {
_29
Counter.increment()
_29
}
_29
}
_29
`,
_29
proposer: fcl.currentUser,
_29
payer: fcl.currentUser,
_29
authorizations: [fcl.currentUser.authorization],
_29
limit: 50,
_29
});
_29
_29
console.log("Transaction Id", transactionId);
_29
_29
await fcl.tx(transactionId).onceSealed();
_29
console.log("Transaction Sealed");
_29
_29
queryCount();
_29
} catch (error) {
_29
console.error("Transaction Failed", error);
_29
}
_29
};

  • Explanation:
    • fcl.mutate is used to send a transaction to the Flow blockchain.
    • The Cadence transaction imports the Counter contract and calls increment().
    • proposer, payer, and authorizations are set to fcl.currentUser, meaning the authenticated user will sign and pay for the transaction.
    • We wait for the transaction to be sealed (completed and finalized on the blockchain) using fcl.tx(transactionId).onceSealed().
    • After the transaction is sealed, we call queryCount() to fetch the updated count.

Next, update the return statement to include the button for incrementing the count:


_11
{user.loggedIn ? (
_11
<div>
_11
<p>Address: {user.addr}</p>
_11
<button onClick={logOut}>Log Out</button>
_11
<div>
_11
<button onClick={incrementCount}>Increment Count</button>
_11
</div>
_11
</div>
_11
) : (
_11
<button onClick={logIn}>Log In</button>
_11
)}

  • Explanation:
    • When the user is logged in, we display a button: "Increment Count".
    • Clicking this button triggers the incrementCount function to send a transaction to increment the count.

Full Code

Your pages/index.js should now look like this:


_88
// pages/index.js
_88
_88
import { useState, useEffect } from "react";
_88
import * as fcl from "@onflow/fcl";
_88
_88
export default function Home() {
_88
const [count, setCount] = useState(0);
_88
const [user, setUser] = useState({ loggedIn: false });
_88
_88
const queryCount = async () => {
_88
try {
_88
const res = await fcl.query({
_88
cadence: `
_88
import Counter from 0x8a4dce54554b225d
_88
_88
access(all)
_88
fun main(): Int {
_88
return Counter.getCount()
_88
}
_88
`,
_88
});
_88
setCount(res);
_88
} catch (error) {
_88
console.error("Error querying count:", error);
_88
}
_88
};
_88
_88
useEffect(() => {
_88
fcl.currentUser.subscribe(setUser);
_88
queryCount();
_88
}, []);
_88
_88
const logIn = () => {
_88
fcl.authenticate();
_88
};
_88
_88
const logOut = () => {
_88
fcl.unauthenticate();
_88
};
_88
_88
const incrementCount = async () => {
_88
try {
_88
const transactionId = await fcl.mutate({
_88
cadence: `
_88
import Counter from 0x8a4dce54554b225d
_88
_88
transaction {
_88
prepare(acct: AuthAccount) {}
_88
execute {
_88
Counter.increment()
_88
}
_88
}
_88
`,
_88
proposer: fcl.currentUser,
_88
payer: fcl.currentUser,
_88
authorizations: [fcl.currentUser.authorization],
_88
limit: 50,
_88
});
_88
_88
console.log("Transaction Id", transactionId);
_88
_88
await fcl.tx(transactionId).onceSealed();
_88
console.log("Transaction Sealed");
_88
_88
queryCount();
_88
} catch (error) {
_88
console.error("Transaction Failed", error);
_88
}
_88
};
_88
_88
return (
_88
<div>
_88
<h1>FCL App Quickstart</h1>
_88
<div>Count: {count}</div>
_88
{user.loggedIn ? (
_88
<div>
_88
<p>Address: {user.addr}</p>
_88
<button onClick={logOut}>Log Out</button>
_88
<div>
_88
<button onClick={incrementCount}>Increment Count</button>
_88
</div>
_88
</div>
_88
) : (
_88
<button onClick={logIn}>Log In</button>
_88
)}
_88
</div>
_88
);
_88
}

Running the App

Now, run your app with npm run dev and open it in your browser at http://localhost:3000.

  • Log In:

    • Click the "Log In" button.
    • The Discovery UI will appear, presenting you with a list of wallets to authenticate with (e.g., Flow Testnet Wallet).
    • Select a wallet and follow the prompts to log in.
  • Increment Count:

    • Once logged in, you'll see your account address displayed.
    • Click the "Increment Count" button.
    • Your wallet will prompt you to approve the transaction.
    • After approving, the transaction will be sent to the Flow blockchain.
  • View Updated Count:

    • After the transaction is sealed, the app will automatically fetch and display the updated count.
    • You should see the count updated on the page.

By following these steps, you've successfully created a simple frontend application using Next.js that interacts with the Counter smart contract on the Flow blockchain. You've learned how to read data from the blockchain, authenticate users, and send transactions to mutate the state of a smart contract.