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 TypeScript: Yes
- Use src directory: Yes
- Use App Router: Yes
This command creates a new Next.js project named kit-app-quickstart
inside your current directory. We’re generating the frontend in a subdirectory so we can next move it into our existing project structure from the previous steps.
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
You should already have the Flow emulator running from the local development step. If it's not running, you can start it again — but note that restarting the emulator will clear all blockchain state, including any contracts deployed in Step 2: Local Development.
Start the Flow Emulator (if not already running)
Open a new terminal window in your project directory and run:
_10flow emulator start
This will start the Flow emulator on http://localhost:8888
. Make sure to keep it running in a separate terminal.
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:
_24// src/app/layout.tsx_24"use client";_24_24import { FlowProvider } from "@onflow/kit";_24import flowJSON from "../../flow.json";_24_24export default function RootLayout({ children }: { children: React.ReactNode }) {_24 return (_24 <html>_24 <body>_24 <FlowProvider_24 config={{_24 accessNodeUrl: "http://localhost:8888",_24 flowNetwork: "emulator",_24 discoveryWallet: "https://fcl-discovery.onflow.org/emulator/authn",_24 }}_24 flowJson={flowJSON}_24 >_24 {children}_24 </FlowProvider>_24 </body>_24 </html>_24 );_24}
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.
Interacting With the Chain
Now that we've set our provider, lets start interacting with the chain.
Querying the Chain
First, use the kit’s useFlowQuery
hook to read the current counter value from the blockchain.
_18import { useFlowQuery } from "@onflow/kit";_18_18const { data, isLoading, error, refetch } = useFlowQuery({_18 cadence: `_18 import "Counter"_18 import "NumberFormatter"_18_18 access(all)_18 fun main(): String {_18 let count: Int = Counter.getCount()_18 let formattedCount = NumberFormatter.formatWithCommas(number: count)_18 return formattedCount_18 }_18 `,_18 enabled: true,_18});_18_18// 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.
- Import Syntax: The imports (
import "Counter"
andimport "NumberFormatter"
) don’t include addresses because those are automatically resolved using theflow.json
file configured in yourFlowProvider
. This keeps your Cadence scripts portable and environment-independent. enabled
Flag: This controls whether the query should run automatically. Set it totrue
to run on mount, or pass a condition (e.g.!!user?.addr
) to delay execution until the user is available. This is useful for queries that depend on authentication or other asynchronous data.
Sending a Transaction
Next, use the kit’s useFlowMutate
hook to send a transaction that increments the counter.
_27import { useFlowMutate } from "@onflow/kit";_27_27const {_27 mutate: increment,_27 isPending: txPending,_27 data: txId,_27 error: txError,_27} = useFlowMutate();_27_27const handleIncrement = () => {_27 increment({_27 cadence: `_27 import "Counter"_27_27 transaction {_27 prepare(acct: &Account) {_27 // Authorization handled via wallet_27 }_27 execute {_27 Counter.increment()_27 let newCount = Counter.getCount()_27 log("New count after incrementing: ".concat(newCount.toString()))_27 }_27 }_27 `,_27 });_27};
Explanation
This sends a Cadence transaction to the blockchain using the mutate
function. The transaction imports the Counter
contract and calls its increment
function. Authorization is handled automatically by the connected wallet during the prepare
phase. Once submitted, the returned txId
can be used to track the transaction's status in real time.
Subscribing to Transaction Status
Use the kit’s [useFlowTransaction
] hook to monitor and display the transaction status in real time.
_11const { transactionStatus, error: txStatusError } = useFlowTransaction(_11 txId || "",_11);_11_11useEffect(() => {_11 if (txId && transactionStatus?.status === 3) {_11 refetch();_11 }_11}, [transactionStatus?.status, txId, refetch]);_11_11// You can then use transactionStatus (for example, its statusString) to show updates.
Explanation:
useFlowTransaction(txId)
subscribes to real-time updates about a transaction's lifecycle using the transaction ID.transactionStatus.status
is a numeric code representing the state of the transaction:0
: Unknown – The transaction status is not yet known.1
: Pending – The transaction has been submitted and is waiting to be included in a block.2
: Finalized – The transaction has been included in a block, but not yet executed.3
: Executed – The transaction code has run successfully, but the result has not yet been sealed.4
: Sealed – The transaction is fully complete, included in a block, and now immutable on-chain.
- We recommend calling
refetch()
when the status reaches 3 (Executed) to update your UI more quickly after the transaction runs, rather than waiting for sealing. - The
statusString
property gives a human-readable version of the current status you can display in the UI.
Why Executed
is Recommended for UI Updates:
Waiting for Sealed
provides full on-chain confirmation but can introduce a delay — especially in local or test environments. Since most transactions (like incrementing a counter) don't require strong finality guarantees, you can typically refetch data once the transaction reaches Executed
for a faster, more responsive user experience.
However:
- If you're dealing with critical state changes (e.g., token transfers or contract deployments), prefer waiting for
Sealed
. - For non-critical UI updates,
Executed
is usually safe and significantly improves perceived performance.
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.
_114// src/app/page.js_114_114"use client";_114_114import { useState, useEffect } from "react";_114import {_114 useFlowQuery,_114 useFlowMutate,_114 useFlowTransaction,_114 useCurrentFlowUser,_114} from "@onflow/kit";_114_114export default function Home() {_114 const { user, authenticate, unauthenticate } = useCurrentFlowUser();_114 const [lastTxId, setLastTxId] = useState<string>();_114_114 const { data, isLoading, error, refetch } = useFlowQuery({_114 cadence: `_114 import "Counter"_114 import "NumberFormatter"_114_114 access(all)_114 fun main(): String {_114 let count: Int = Counter.getCount()_114 let formattedCount = NumberFormatter.formatWithCommas(number: count)_114 return formattedCount_114 }_114 `,_114 enabled: true,_114 });_114_114 const {_114 mutate: increment,_114 isPending: txPending,_114 data: txId,_114 error: txError,_114 } = useFlowMutate();_114_114 const { transactionStatus, error: txStatusError } = useFlowTransaction(_114 txId || "",_114 );_114_114 useEffect(() => {_114 if (txId && transactionStatus?.status === 4) {_114 refetch();_114 }_114 }, [transactionStatus?.status, txId, refetch]);_114_114 const handleIncrement = () => {_114 increment({_114 cadence: `_114 import "Counter"_114_114 transaction {_114 prepare(acct: &Account) {_114 // Authorization handled via wallet_114 }_114 execute {_114 Counter.increment()_114 let newCount = Counter.getCount()_114 log("New count after incrementing: ".concat(newCount.toString()))_114 }_114 }_114 `,_114 });_114 };_114_114 return (_114 <div>_114 <h1>@onflow/kit App Quickstart</h1>_114_114 {isLoading ? (_114 <p>Loading count...</p>_114 ) : error ? (_114 <p>Error fetching count: {error.message}</p>_114 ) : (_114 <div>_114 <h2>Count: {data as string}</h2>_114 </div>_114 )}_114_114 {user.loggedIn ? (_114 <div>_114 <p>Address: {user.addr}</p>_114 <button onClick={unauthenticate}>Log Out</button>_114 <button onClick={handleIncrement} disabled={txPending}>_114 {txPending ? "Processing..." : "Increment Count"}_114 </button>_114_114 <div>_114 Latest Transaction Status:{" "}_114 {transactionStatus?.statusString || "No transaction yet"}_114 </div>_114_114 {txError && <p>Error sending transaction: {txError.message}</p>}_114_114 {lastTxId && (_114 <div>_114 <h3>Transaction Status</h3>_114 {transactionStatus ? (_114 <p>Status: {transactionStatus.statusString}</p>_114 ) : (_114 <p>Waiting for status update...</p>_114 )}_114 {txStatusError && <p>Error: {txStatusError.message}</p>}_114 </div>_114 )}_114 </div>_114 ) : (_114 <button onClick={authenticate}>Log In</button>_114 )}_114 </div>_114 );_114}
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.