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 local Flow emulator. Using the Flow Client Library (FCL), you'll learn how to read and modify the contract's state from a React web application, set up wallet authentication using FCL's Discovery UI connected to the local emulator, and query the chain to read data from smart contracts.

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.
  • Query the chain to read data from smart contracts on the local emulator.
  • Mutate the state of a smart contract by sending transactions using FCL and a wallet connected to the local emulator.
  • Set up the Discovery UI to use a wallet for authentication with the local emulator.

Prerequisites

Setting Up the Next.js App

Assuming you're in your project directory from Steps 1 and 2, we'll create a Next.js frontend application to interact with your smart contract deployed on the local Flow emulator.

Step 1: Create a New Next.js App

First, we'll create a new Next.js application using npx create-next-app. We'll create it inside your existing project directory and then move it up to the root directory.

Assumption: You are already in your project directory.

Run the following command:


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

During the setup process, you'll be prompted with several options. Choose the following:

  • TypeScript: No
  • Use src directory: Yes
  • Use App Router: Yes

This command will create a new Next.js project named fcl-app-quickstart inside your current directory.

Step 2: Move the Next.js App Up a Directory

Now, we'll move the contents of the fcl-app-quickstart directory up to your project root directory.

Note: Moving the Next.js app into your existing project may overwrite existing files such as package.json, package-lock.json, .gitignore, etc. Make sure to back up any important files before proceeding. You may need to merge configurations manually.

Remove the README File

Before moving the files, let's remove the README.md file from the fcl-app-quickstart directory to avoid conflicts:


_10
rm fcl-app-quickstart/README.md

Merge .gitignore Files and Move Contents

To merge the .gitignore files, you can use the cat command to concatenate them and then remove duplicates:


_10
cat .gitignore fcl-app-quickstart/.gitignore | sort | uniq > temp_gitignore
_10
mv temp_gitignore .gitignore

Now, move the contents of the fcl-app-quickstart directory to your project root:

On macOS/Linux:


_10
mv fcl-app-quickstart/* .
_10
mv fcl-app-quickstart/.* . # This moves hidden files like .env.local if any
_10
rm -r fcl-app-quickstart

On Windows (PowerShell):


_10
Move-Item -Path .\fcl-app-quickstart\* -Destination . -Force
_10
Move-Item -Path .\fcl-app-quickstart\.* -Destination . -Force
_10
Remove-Item -Recurse -Force .\fcl-app-quickstart

Note: When moving hidden files (those starting with a dot, like .gitignore), ensure you don't overwrite important files in your root directory.

Step 3: Install FCL

Now, install the Flow Client Library (FCL) in your project. FCL is a JavaScript library that simplifies interaction with the Flow blockchain:


_10
npm install @onflow/fcl

Setting Up the Local Flow Emulator and Dev Wallet

Before proceeding, ensure that both the Flow emulator and the Dev Wallet are running.

Step 1: Start the Flow Emulator

In a new terminal window, navigate to your project directory and run:


_10
flow emulator start

This starts the Flow emulator on http://localhost:8888.

Step 2: Start the FCL Dev Wallet

In another terminal window, run:


_10
flow dev-wallet

This starts the Dev Wallet, which listens on http://localhost:8701. The Dev Wallet is a local wallet that allows you to authenticate with the Flow blockchain and sign transactions on the local emulator. This is the wallet we'll select in Discovery UI when authenticating.

Querying the Chain

Now, let's read data from the Counter smart contract deployed on the local Flow emulator.

Since you've already deployed the Counter contract in Step 2: Local Development, we can proceed to query it.

Step 1: Update the Home Page

Open src/app/page.js in your editor.

Adding the FCL Configuration Before the Rest

At the top of your page.js file, before the rest of the code, we'll add the FCL configuration. This ensures that FCL is properly configured before we use it.

Add the following code:


_10
import * as fcl from "@onflow/fcl";
_10
_10
// FCL Configuration
_10
fcl.config({
_10
...fcl.flowEmulator
_10
});

This configuration code sets up FCL to work with the local Flow emulator and Dev Wallet. The flowEmulator object provides a default FCL configuration that simplifies development by automatically setting the necessary parameters. You can learn more about default configurations here.

For more information on Discovery configurations, refer to the Wallet Discovery Guide.

Implementing the Component

Now, we'll implement the component to query the count from the Counter contract.

Update your page.js file to the following:


_52
// src/app/page.js
_52
_52
"use client"; // This directive is necessary when using useState and useEffect in Next.js App Router
_52
_52
import { useState, useEffect } from "react";
_52
import * as fcl from "@onflow/fcl";
_52
_52
// FCL Configuration
_52
fcl.config({
_52
...fcl.flowEmulator
_52
});
_52
_52
export default function Home() {
_52
const [count, setCount] = useState(0);
_52
_52
const queryCount = async () => {
_52
try {
_52
const res = await fcl.query({
_52
cadence: `
_52
import Counter from 0xf8d6e0586b0a20c7
_52
import NumberFormatter from 0xf8d6e0586b0a20c7
_52
_52
access(all)
_52
fun main(): String {
_52
// Retrieve the count from the Counter contract
_52
let count: Int = Counter.getCount()
_52
_52
// Format the count using NumberFormatter
_52
let formattedCount = NumberFormatter.formatWithCommas(number: count)
_52
_52
// Return the formatted count
_52
return formattedCount
_52
}
_52
`,
_52
});
_52
setCount(res);
_52
} catch (error) {
_52
console.error("Error querying count:", error);
_52
}
_52
};
_52
_52
useEffect(() => {
_52
queryCount();
_52
}, []);
_52
_52
return (
_52
<div>
_52
<h1>FCL App Quickstart</h1>
_52
<div>Count: {count}</div>
_52
</div>
_52
);
_52
}

In the above code:

  • We import the necessary React hooks (useState and useEffect) and the FCL library.
  • We define the Home component, which is the main page of our app.
  • We set up a state variable count using the useState hook to store the count value.
  • We define an async function queryCount to query the count from the Counter contract.
  • We use the useEffect hook to call queryCount when the component mounts.
  • We return a simple JSX structure that displays the count value on the page.
  • If an error occurs during the query, we log it to the console.
  • We use the script from Step 2 to query the count from the Counter contract and format it using the NumberFormatter contract.
info

In this tutorial, we've shown you hardcoding addresses directly for simplicity and brevity. However, it's recommended to use the import "ContractName" syntax, as demonstrated in Step 2: Local Development. This approach is supported by the Flow Client Library (FCL) and allows you to use aliases for contract addresses in your flow.json file. It makes your code more flexible, maintainable, and easier to adapt across different environments (e.g., testnet, mainnet).

Learn more about this best practice in the FCL Documentation.

Step 2: Run the App

Start your development server:


_10
npm run dev

Visit http://localhost:3000 in your browser. You should see the current count displayed on the page, formatted according to the NumberFormatter contract.

Mutating the Chain State

Now that we've successfully read data from the Flow blockchain emulator, let's modify the state by incrementing the count in the Counter contract. We'll set up wallet authentication and send a transaction to the blockchain emulator.

Adding Authentication and Transaction Functionality

Step 1: Manage Authentication State

In src/app/page.js, add new state variables to manage the user's authentication state:


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

Step 2: Subscribe to Authentication Changes

Update the useEffect hook to subscribe to the current user's authentication state:


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

The currentUser.subscribe method listens for changes to the current user's authentication state and updates the user state accordingly.

Step 3: Define Log In and Log Out Functions

Define the logIn and logOut functions:


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

The authenticate method opens the Discovery UI for the user to log in, while unauthenticate logs the user out.

Step 4: Define the incrementCount Function

Add the incrementCount function:


_38
const incrementCount = async () => {
_38
try {
_38
const transactionId = await fcl.mutate({
_38
cadence: `
_38
import Counter from 0xf8d6e0586b0a20c7
_38
_38
transaction {
_38
_38
prepare(acct: &Account) {
_38
// Authorizes the transaction
_38
}
_38
_38
execute {
_38
// Increment the counter
_38
Counter.increment()
_38
_38
// Retrieve the new count and log it
_38
let newCount = Counter.getCount()
_38
log("New count after incrementing: ".concat(newCount.toString()))
_38
}
_38
}
_38
`,
_38
proposer: fcl.currentUser,
_38
payer: fcl.currentUser,
_38
authorizations: [fcl.currentUser.authorization],
_38
limit: 50,
_38
});
_38
_38
console.log("Transaction Id", transactionId);
_38
_38
await fcl.tx(transactionId).onceExecuted();
_38
console.log("Transaction Executed");
_38
_38
queryCount();
_38
} catch (error) {
_38
console.error("Transaction Failed", error);
_38
}
_38
};

In the above code:

  • We define an async function incrementCount to send a transaction to increment the count in the Counter contract.
  • We use the mutate method to send a transaction to the blockchain emulator.
  • The transaction increments the count in the Counter contract and logs the new count.
  • We use the proposer, payer, and authorizations properties to set the transaction's proposer, payer, and authorizations to the current user.
  • The limit property sets the gas limit for the transaction.
  • We log the transaction ID and wait for the transaction to be sealed before querying the updated count.
  • If an error occurs during the transaction, we log it to the console.
  • After the transaction is sealed, we call queryCount to fetch and display the updated count.
  • We use the transaction from Step 2 to increment the count in the Counter contract.

Step 5: Update the Return Statement

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


_17
return (
_17
<div>
_17
<h1>FCL App Quickstart</h1>
_17
<div>Count: {count}</div>
_17
{user.loggedIn ? (
_17
<div>
_17
<p>Address: {user.addr}</p>
_17
<button onClick={logOut}>Log Out</button>
_17
<div>
_17
<button onClick={incrementCount}>Increment Count</button>
_17
</div>
_17
</div>
_17
) : (
_17
<button onClick={logIn}>Log In</button>
_17
)}
_17
</div>
_17
);

Full page.js Code

Your src/app/page.js should now look like this:


_112
// src/app/page.js
_112
_112
"use client";
_112
_112
import { useState, useEffect } from "react";
_112
import * as fcl from "@onflow/fcl";
_112
_112
// FCL Configuration
_112
fcl.config({
_112
...fcl.flowEmulator
_112
});
_112
_112
export default function Home() {
_112
const [count, setCount] = useState(0);
_112
const [user, setUser] = useState({ loggedIn: false });
_112
_112
const queryCount = async () => {
_112
try {
_112
const res = await fcl.query({
_112
cadence: `
_112
import Counter from 0xf8d6e0586b0a20c7
_112
import NumberFormatter from 0xf8d6e0586b0a20c7
_112
_112
access(all)
_112
fun main(): String {
_112
// Retrieve the count from the Counter contract
_112
let count: Int = Counter.getCount()
_112
_112
// Format the count using NumberFormatter
_112
let formattedCount = NumberFormatter.formatWithCommas(number: count)
_112
_112
// Return the formatted count
_112
return formattedCount
_112
}
_112
`,
_112
});
_112
setCount(res);
_112
} catch (error) {
_112
console.error("Error querying count:", error);
_112
}
_112
};
_112
_112
useEffect(() => {
_112
fcl.currentUser.subscribe(setUser);
_112
queryCount();
_112
}, []);
_112
_112
const logIn = () => {
_112
fcl.authenticate();
_112
};
_112
_112
const logOut = () => {
_112
fcl.unauthenticate();
_112
};
_112
_112
const incrementCount = async () => {
_112
try {
_112
const transactionId = await fcl.mutate({
_112
cadence: `
_112
import Counter from 0xf8d6e0586b0a20c7
_112
_112
transaction {
_112
_112
prepare(acct: &Account) {
_112
// Authorizes the transaction
_112
}
_112
_112
execute {
_112
// Increment the counter
_112
Counter.increment()
_112
_112
// Retrieve the new count and log it
_112
let newCount = Counter.getCount()
_112
log("New count after incrementing: ".concat(newCount.toString()))
_112
}
_112
}
_112
`,
_112
proposer: fcl.currentUser,
_112
payer: fcl.currentUser,
_112
authorizations: [fcl.currentUser.authorization],
_112
limit: 50,
_112
});
_112
_112
console.log("Transaction Id", transactionId);
_112
_112
await fcl.tx(transactionId).onceExecuted();
_112
console.log("Transaction Executed");
_112
_112
queryCount();
_112
} catch (error) {
_112
console.error("Transaction Failed", error);
_112
}
_112
};
_112
_112
return (
_112
<div>
_112
<h1>FCL App Quickstart</h1>
_112
<div>Count: {count}</div>
_112
{user.loggedIn ? (
_112
<div>
_112
<p>Address: {user.addr}</p>
_112
<button onClick={logOut}>Log Out</button>
_112
<div>
_112
<button onClick={incrementCount}>Increment Count</button>
_112
</div>
_112
</div>
_112
) : (
_112
<button onClick={logIn}>Log In</button>
_112
)}
_112
</div>
_112
);
_112
}

Visit http://localhost:3000 in your browser.

  • Log In:

    • Click the "Log In" button.
    • The Discovery UI will appear, showing the available wallets. Select the "Dev Wallet" option.
    • Select the account to log in with.
    • If prompted, create a new account or use an existing one.
  • Increment Count:

    • After logging in, you'll see your account address displayed.
    • Click the "Increment Count" button.
    • Your wallet will prompt you to approve the transaction.
    • Approve the transaction to send it to the Flow emulator.
  • View Updated Count:

    • Once the transaction is sealed, the app will automatically fetch and display the updated count.
    • You should see the count incremented on the page, formatted using the NumberFormatter contract.

Conclusion

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 emulator. You've learned how to:

  • Add the FCL configuration before the rest of your code within the page.js file.
  • Configure FCL to work with the local Flow emulator and Dev Wallet.
  • Start the Dev Wallet using flow dev-wallet to enable local authentication.
  • Read data from the local blockchain emulator, utilizing multiple contracts (Counter and NumberFormatter).
  • Authenticate users using the local Dev Wallet.
  • Send transactions to mutate the state of a smart contract on the local emulator.

Additional Resources