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:
_10npx create-next-app@latest fcl-app-quickstart_10cd 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_10export 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:
_10npm 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:
_10npm 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_10import * as fcl from "@onflow/fcl";_10_10fcl.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_10import "../styles/globals.css";_10import "../flow/config"; // Import the FCL configuration_10_10function MyApp({ Component, pageProps }) {_10 return <Component {...pageProps} />;_10}_10_10export 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_10import { useState, useEffect } from "react";_10import * as fcl from "@onflow/fcl";
Next, initialize the count
state variable inside your Home
component:
_10const [count, setCount] = useState(0);
Now, let's create a function to query the count from the blockchain:
_17const 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 amain
function that returns thecount
variable via thegetCount()
function. - The result of the query is stored in
res
, and we update thecount
state withsetCount(res)
.
- We use
Next, use the useEffect
hook to call queryCount
when the component mounts:
_10useEffect(() => {_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:
_10return (_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_37import { useState, useEffect } from "react";_37import * as fcl from "@onflow/fcl";_37_37export 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
:
_10fcl.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:
_10const [user, setUser] = useState({ loggedIn: false });
Then, use useEffect
to subscribe to the current user's authentication state:
_10useEffect(() => {_10 fcl.currentUser.subscribe(setUser);_10 queryCount();_10}, []);
- Explanation:
fcl.currentUser.subscribe(setUser)
sets up a listener that updates theuser
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:
_10const logIn = () => {_10 fcl.authenticate();_10};_10_10const 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:
_15return (_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:
_29const 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 callsincrement()
. proposer
,payer
, andauthorizations
are set tofcl.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_88import { useState, useEffect } from "react";_88import * as fcl from "@onflow/fcl";_88_88export 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.