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
- Completion of Step 1: Contract Interaction and Step 2: Local Development.
- Flow CLI installed.
- Node.js and npm installed.
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:
_10npx 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:
_10rm 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:
_10cat .gitignore fcl-app-quickstart/.gitignore | sort | uniq > temp_gitignore_10mv temp_gitignore .gitignore
Now, move the contents of the fcl-app-quickstart
directory to your project root:
On macOS/Linux:
_10mv fcl-app-quickstart/* ._10mv fcl-app-quickstart/.* . # This moves hidden files like .env.local if any_10rm -r fcl-app-quickstart
On Windows (PowerShell):
_10Move-Item -Path .\fcl-app-quickstart\* -Destination . -Force_10Move-Item -Path .\fcl-app-quickstart\.* -Destination . -Force_10Remove-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:
_10npm 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:
_10flow emulator start
This starts the Flow emulator on http://localhost:8888
.
Step 2: Start the FCL Dev Wallet
In another terminal window, run:
_10flow 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:
_10import * as fcl from "@onflow/fcl";_10_10// FCL Configuration_10fcl.config({_10 "flow.network": "local",_10 "accessNode.api": "http://localhost:8888", // Flow Emulator_10 "discovery.wallet": "http://localhost:8701/fcl/authn", // Local Wallet Discovery_10});
This configuration code sets up FCL to work with the local Flow emulator and Dev Wallet. The flow.network
and accessNode.api
properties point to the local emulator, while discovery.wallet
points to the local Dev Wallet for authentication.
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:
_54// src/app/page.js_54_54"use client"; // This directive is necessary when using useState and useEffect in Next.js App Router_54_54import { useState, useEffect } from "react";_54import * as fcl from "@onflow/fcl";_54_54// FCL Configuration_54fcl.config({_54 "flow.network": "local",_54 "accessNode.api": "http://localhost:8888",_54 "discovery.wallet": "http://localhost:8701/fcl/authn", // Local Dev Wallet_54});_54_54export default function Home() {_54 const [count, setCount] = useState(0);_54_54 const queryCount = async () => {_54 try {_54 const res = await fcl.query({_54 cadence: `_54 import Counter from 0xf8d6e0586b0a20c7_54 import NumberFormatter from 0xf8d6e0586b0a20c7_54 _54 access(all)_54 fun main(): String {_54 // Retrieve the count from the Counter contract_54 let count: Int = Counter.getCount()_54 _54 // Format the count using NumberFormatter_54 let formattedCount = NumberFormatter.formatWithCommas(number: count)_54 _54 // Return the formatted count_54 return formattedCount_54 }_54 `,_54 });_54 setCount(res);_54 } catch (error) {_54 console.error("Error querying count:", error);_54 }_54 };_54_54 useEffect(() => {_54 queryCount();_54 }, []);_54_54 return (_54 <div>_54 <h1>FCL App Quickstart</h1>_54 <div>Count: {count}</div>_54 </div>_54 );_54}
In the above code:
- We import the necessary React hooks (
useState
anduseEffect
) 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 theuseState
hook to store the count value. - We define an
async
functionqueryCount
to query the count from theCounter
contract. - We use the
useEffect
hook to callqueryCount
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 theNumberFormatter
contract.
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:
_10npm 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:
_10const [user, setUser] = useState({ loggedIn: false });
Step 2: Subscribe to Authentication Changes
Update the useEffect
hook to subscribe to the current user's authentication state:
_10useEffect(() => {_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:
_10const logIn = () => {_10 fcl.authenticate();_10};_10_10const 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:
_38const 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).onceSealed();_38 console.log("Transaction Sealed");_38_38 queryCount();_38 } catch (error) {_38 console.error("Transaction Failed", error);_38 }_38};
In the above code:
- We define an
async
functionincrementCount
to send a transaction to increment the count in theCounter
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
, andauthorizations
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:
_17return (_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:
_114// src/app/page.js_114_114"use client";_114_114import { useState, useEffect } from "react";_114import * as fcl from "@onflow/fcl";_114_114// FCL Configuration_114fcl.config({_114 "flow.network": "local",_114 "accessNode.api": "http://localhost:8888",_114 "discovery.wallet": "http://localhost:8701/fcl/authn", // Local Dev Wallet_114});_114_114export default function Home() {_114 const [count, setCount] = useState(0);_114 const [user, setUser] = useState({ loggedIn: false });_114_114 const queryCount = async () => {_114 try {_114 const res = await fcl.query({_114 cadence: `_114 import Counter from 0xf8d6e0586b0a20c7_114 import NumberFormatter from 0xf8d6e0586b0a20c7_114 _114 access(all)_114 fun main(): String {_114 // Retrieve the count from the Counter contract_114 let count: Int = Counter.getCount()_114 _114 // Format the count using NumberFormatter_114 let formattedCount = NumberFormatter.formatWithCommas(number: count)_114 _114 // Return the formatted count_114 return formattedCount_114 }_114 `,_114 });_114 setCount(res);_114 } catch (error) {_114 console.error("Error querying count:", error);_114 }_114 };_114_114 useEffect(() => {_114 fcl.currentUser.subscribe(setUser);_114 queryCount();_114 }, []);_114_114 const logIn = () => {_114 fcl.authenticate();_114 };_114_114 const logOut = () => {_114 fcl.unauthenticate();_114 };_114_114 const incrementCount = async () => {_114 try {_114 const transactionId = await fcl.mutate({_114 cadence: `_114 import Counter from 0xf8d6e0586b0a20c7_114_114 transaction {_114_114 prepare(acct: &Account) {_114 // Authorizes the transaction_114 }_114 _114 execute {_114 // Increment the counter_114 Counter.increment()_114 _114 // Retrieve the new count and log it_114 let newCount = Counter.getCount()_114 log("New count after incrementing: ".concat(newCount.toString()))_114 }_114 }_114 `,_114 proposer: fcl.currentUser,_114 payer: fcl.currentUser,_114 authorizations: [fcl.currentUser.authorization],_114 limit: 50,_114 });_114_114 console.log("Transaction Id", transactionId);_114_114 await fcl.tx(transactionId).onceSealed();_114 console.log("Transaction Sealed");_114_114 queryCount();_114 } catch (error) {_114 console.error("Transaction Failed", error);_114 }_114 };_114_114 return (_114 <div>_114 <h1>FCL App Quickstart</h1>_114 <div>Count: {count}</div>_114 {user.loggedIn ? (_114 <div>_114 <p>Address: {user.addr}</p>_114 <button onClick={logOut}>Log Out</button>_114 <div>_114 <button onClick={incrementCount}>Increment Count</button>_114 </div>_114 </div>_114 ) : (_114 <button onClick={logIn}>Log In</button>_114 )}_114 </div>_114 );_114}
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
andNumberFormatter
). - Authenticate users using the local Dev Wallet.
- Send transactions to mutate the state of a smart contract on the local emulator.