Simple Frontend with @onflow/kit
Building on the Counter
contract you deployed in Step 1: Contract Interaction and Step 2: Local Development, this tutorial shows you how to create a simple Next.js frontend that interacts with the Counter
smart contract deployed on your local Flow emulator. Instead of using FCL directly, you'll leverage @onflow/kit to simplify authentication, querying, transactions, and to display real-time transaction status updates using convenient React hooks.
Objectives
After finishing this guide you will be able to:
- Wrap your Next.js app with a Flow provider using @onflow/kit.
- Read data from a Cadence smart contract (
Counter
) using kit’s query hook. - Send a transaction to update the smart contract’s state using kit’s mutation hook.
- Monitor a transaction’s status in real time using kit’s transaction hook.
- Authenticate with the Flow blockchain using kit’s built-in hooks and the local Dev Wallet.
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
Follow these steps to set up your Next.js project and integrate @onflow/kit.
Step 1: Create a New Next.js App
Run the following command in your project directory:
_10npx create-next-app@latest kit-app-quickstart
During setup, choose the following options:
- Use src directory: Yes
- Use App Router: Yes
This command creates a new Next.js project named kit-app-quickstart
inside your current directory.
Step 2: Move the Next.js App Up a Directory
Move the contents of the kit-app-quickstart
directory into your project root. For example:
On macOS/Linux:
_10mv kit-app-quickstart/* ._10mv kit-app-quickstart/.* . # To move hidden files (e.g. .env.local)_10rm -r kit-app-quickstart
On Windows (PowerShell):
_10Move-Item -Path .\kit-app-quickstart\* -Destination . -Force_10Move-Item -Path .\kit-app-quickstart\.* -Destination . -Force_10Remove-Item -Recurse -Force .\kit-app-quickstart
Note: When moving hidden files (those beginning with a dot) like .gitignore
, be cautious not to overwrite any important files.
Step 3: Install @onflow/kit
Install the kit library in your project:
_10npm install @onflow/kit
This library wraps FCL internally and exposes a set of hooks for authentication, querying, sending transactions, and tracking transaction status.
Configuring the Local Flow Emulator and Dev Wallet
Before moving on, ensure that both the Flow emulator and the Dev Wallet are running.
Step 1: Start the Flow Emulator
Open a new terminal window in your project directory and run:
_10flow emulator start
This will start the Flow emulator on http://localhost:8888
.
Step 2: Start the Dev Wallet
In another terminal window, run:
_10flow dev-wallet
This will start the Dev Wallet on http://localhost:8701
, which you’ll use for authentication during development.
Wrapping Your App with FlowProvider
@onflow/kit provides a FlowProvider
component that sets up the Flow Client Library configuration. In Next.js using the App Router, add or update your src/app/layout.tsx
as follows:
_23// src/app/layout.tsx_23_23import { FlowProvider } from "@onflow/kit";_23import flowJSON from "../flow.json";_23_23export default function RootLayout({ children }: { children: React.ReactNode }) {_23 return (_23 <html>_23 <body>_23 <FlowProvider_23 config={{_23 accessNodeUrl: "http://localhost:8888",_23 flowNetwork: "emulator",_23 discoveryWallet: "https://fcl-discovery.onflow.org/emulator/authn",_23 }}_23 flowJson={flowJSON}_23 >_23 {children}_23 </FlowProvider>_23 </body>_23 </html>_23 );_23}
This configuration initializes the kit with your local emulator settings and maps contract addresses based on your flow.json
file.
For more information on Discovery configurations, refer to the Wallet Discovery Guide.
Creating the Home Page
We’ll now break the home page creation into four clear steps:
Step 1: Querying the Chain
First, use the kit’s useFlowQuery
hook to read the current counter value from the blockchain.
_23// A snippet demonstrating querying the chain_23import { useFlowQuery } from "@onflow/kit";_23_23const { data: count, isLoading, error, refetch } = useFlowQuery({_23 cadence: `_23 import Counter from 0xf8d6e0586b0a20c7_23 import NumberFormatter from 0xf8d6e0586b0a20c7_23_23 access(all)_23 fun main(): String {_23 // Retrieve the count from the Counter contract_23 let count: Int = Counter.getCount()_23_23 // Format the count using NumberFormatter_23 let formattedCount = NumberFormatter.formatWithCommas(number: count)_23_23 // Return the formatted count_23 return formattedCount_23 }_23 `,_23});_23_23// Use the count data in your component as needed.
This script fetches the counter value, formats it via the NumberFormatter
, and returns the formatted string.
Step 2: Sending a Transaction
Next, use the kit’s useFlowMutate
hook to send a transaction that increments the counter.
_31// A snippet demonstrating sending a transaction_31import { useFlowMutate } from "@onflow/kit";_31_31const { mutate: increment, isPending, error: txError } = useFlowMutate();_31_31const incrementCount = async (user, refetch, setTxId) => {_31 const transactionId = await increment({_31 cadence: `_31 import Counter from 0xf8d6e0586b0a20c7_31_31 transaction {_31 prepare(acct: AuthAccount) {_31 // Authorization handled via the current user_31 }_31 execute {_31 // Increment the counter_31 Counter.increment()_31 // Retrieve and log the new count_31 let newCount = Counter.getCount()_31 log("New count after incrementing: ".concat(newCount.toString()))_31 }_31 }_31 `,_31 proposer: user,_31 payer: user,_31 authorizations: [user.authorization],_31 limit: 50,_31 });_31 setTxId(transactionId);_31 refetch();_31};
In this snippet, after calling the mutation, the returned transaction ID is stored for subscription.
Step 3: Subscribing to Transaction Status
Use the kit’s useFlowTransaction
hook to monitor and display the transaction status in real time.
_10// A snippet demonstrating subscribing to transaction status updates_10import { useFlowTransaction } from "@onflow/kit";_10_10const { transactionStatus, error: txStatusError } = useFlowTransaction(txId);_10_10// You can then use transactionStatus (for example, its statusString) to show updates.
The hook automatically subscribes to the status updates of the transaction identified by txId
.
Step 4: Integrating Authentication and Building the Complete UI
Finally, integrate the query, mutation, and transaction status hooks with authentication using useCurrentFlowUser
. Combine all parts to build the complete page.
_127// src/app/page.js_127_127"use client";_127_127import { useState, useEffect } from "react";_127import {_127 useFlowQuery,_127 useFlowMutate,_127 useFlowTransaction,_127 useCurrentFlowUser,_127} from "@onflow/kit";_127_127export default function Home() {_127 // Authentication: manage user login/logout with kit's useCurrentFlowUser hook._127 const { user, authenticate, unauthenticate } = useCurrentFlowUser();_127_127 // Step 1: Query the current count from the Counter contract._127 const {_127 data: count,_127 isLoading: queryLoading,_127 error: queryError,_127 refetch,_127 } = useFlowQuery({_127 cadence: `_127 import Counter from 0xf8d6e0586b0a20c7_127 import NumberFormatter from 0xf8d6e0586b0a20c7_127_127 access(all)_127 fun main(): String {_127 let count: Int = Counter.getCount()_127 let formattedCount = NumberFormatter.formatWithCommas(number: count)_127 return formattedCount_127 }_127 `,_127 });_127_127 // State variable to store the transaction ID._127 const [txId, setTxId] = useState(null);_127_127 // Step 3: Subscribe to transaction status using the stored txId._127 const { transactionStatus, error: txStatusError } = useFlowTransaction(txId);_127_127 // Automatically refetch the count when the transaction status indicates it is executed (status === 3)._127 useEffect(() => {_127 if (txId && transactionStatus?.status === 3) {_127 refetch();_127 }_127 }, [transactionStatus?.status, txId, refetch]);_127_127 // Step 2: Prepare the mutation for incrementing the counter._127 const { mutate: increment, isPending: txPending, error: txError } = useFlowMutate();_127_127 const handleIncrement = async () => {_127 try {_127 // Send a transaction to increment the counter._127 const transactionId = await increment({_127 cadence: `_127 import Counter from 0xf8d6e0586b0a20c7_127_127 transaction {_127 prepare(acct: AuthAccount) {_127 // Authorization is handled via the current user._127 }_127 execute {_127 Counter.increment()_127 let newCount = Counter.getCount()_127 log("New count after incrementing: ".concat(newCount.toString()))_127 }_127 }_127 `,_127 proposer: user,_127 payer: user,_127 authorizations: [user.authorization],_127 limit: 50,_127 });_127 console.log("Transaction Id", transactionId);_127 setTxId(transactionId);_127 } catch (error) {_127 console.error("Transaction Failed", error);_127 }_127 };_127_127 return (_127 <div>_127 <h1>@onflow/kit App Quickstart</h1>_127_127 {/* Display the queried count */}_127 {queryLoading ? (_127 <p>Loading count...</p>_127 ) : queryError ? (_127 <p>Error fetching count: {queryError.message}</p>_127 ) : (_127 <div>_127 <h2>Count: {count}</h2>_127 <button onClick={refetch}>Refetch Count</button>_127 </div>_127 )}_127_127 {/* Authentication controls and transaction actions */}_127 {user.loggedIn ? (_127 <div>_127 <p>Address: {user.addr}</p>_127 <button onClick={unauthenticate}>Log Out</button>_127 <button onClick={handleIncrement} disabled={txPending}>_127 {txPending ? "Processing..." : "Increment Count"}_127 </button>_127 {txError && <p>Error sending transaction: {txError.message}</p>}_127_127 {/* Display transaction status updates */}_127 {txId && (_127 <div>_127 <h3>Transaction Status</h3>_127 {transactionStatus ? (_127 <p>Status: {transactionStatus.statusString}</p>_127 ) : (_127 <p>Waiting for status update...</p>_127 )}_127 {txStatusError && <p>Error: {txStatusError.message}</p>}_127 </div>_127 )}_127 </div>_127 ) : (_127 <button onClick={authenticate}>Log In</button>_127 )}_127 </div>_127 );_127}
In this complete page:
- Step 1 queries the counter value.
- Step 2 sends a transaction to increment the counter and stores the transaction ID.
- Step 3 subscribes to transaction status updates using the stored transaction ID and uses a
useEffect
hook to automatically refetch the updated count when the transaction is sealed (status code 4). - Step 4 integrates authentication via
useCurrentFlowUser
and combines all the pieces into a single user interface.
Running the App
Start your development server:
_10npm run dev
Then visit http://localhost:3000 in your browser. You should see:
- The current counter value displayed (formatted with commas using
NumberFormatter
). - A Log In button that launches the kit Discovery UI with your local Dev Wallet.
- Once logged in, your account address appears with options to Log Out and Increment Count.
- When you click Increment Count, the transaction is sent; its status updates are displayed in real time below the action buttons, and once the transaction is sealed, the updated count is automatically fetched.
Wrapping Up
By following these steps, you’ve built a simple Next.js dApp that interacts with a Flow smart contract using @onflow/kit. In this guide you learned how to:
- Wrap your application in a
FlowProvider
to configure blockchain connectivity. - Use kit hooks such as
useFlowQuery
,useFlowMutate
,useFlowTransaction
, anduseCurrentFlowUser
to manage authentication, query on-chain data, submit transactions, and monitor their status. - Integrate with the local Flow emulator and Dev Wallet for a fully functional development setup.
For additional details and advanced usage, refer to the @onflow/kit documentation and other Flow developer resources.