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 (useStateanduseEffect) and the FCL library.
- We define the Homecomponent, which is the main page of our app.
- We set up a state variable countusing theuseStatehook to store the count value.
- We define an asyncfunctionqueryCountto query the count from theCountercontract.
- We use the useEffecthook to callqueryCountwhen 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 Countercontract and format it using theNumberFormattercontract.
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 asyncfunctionincrementCountto send a transaction to increment the count in theCountercontract.
- We use the mutatemethod to send a transaction to the blockchain emulator.
- The transaction increments the count in the Countercontract and logs the new count.
- We use the proposer,payer, andauthorizationsproperties to set the transaction's proposer, payer, and authorizations to the current user.
- The limitproperty 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 queryCountto fetch and display the updated count.
- We use the transaction from Step 2 to increment the count in the Countercontract.
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 NumberFormattercontract.
 
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.jsfile.
- Configure FCL to work with the local Flow emulator and Dev Wallet.
- Start the Dev Wallet using flow dev-walletto enable local authentication.
- Read data from the local blockchain emulator, utilizing multiple contracts (CounterandNumberFormatter).
- Authenticate users using the local Dev Wallet.
- Send transactions to mutate the state of a smart contract on the local emulator.