Skip to main content

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

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:


_10
npx 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:


_10
mv kit-app-quickstart/* .
_10
mv kit-app-quickstart/.* . # To move hidden files (e.g. .env.local)
_10
rm -r kit-app-quickstart

On Windows (PowerShell):


_10
Move-Item -Path .\kit-app-quickstart\* -Destination . -Force
_10
Move-Item -Path .\kit-app-quickstart\.* -Destination . -Force
_10
Remove-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:


_10
npm 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:


_10
flow emulator start

This will start the Flow emulator on http://localhost:8888.

Step 2: Start the Dev Wallet

In another terminal window, run:


_10
flow 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
_23
import { FlowProvider } from "@onflow/kit";
_23
import flowJSON from "../flow.json";
_23
_23
export 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
_23
import { useFlowQuery } from "@onflow/kit";
_23
_23
const { 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
_31
import { useFlowMutate } from "@onflow/kit";
_31
_31
const { mutate: increment, isPending, error: txError } = useFlowMutate();
_31
_31
const 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
_10
import { useFlowTransaction } from "@onflow/kit";
_10
_10
const { 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
_127
import { useState, useEffect } from "react";
_127
import {
_127
useFlowQuery,
_127
useFlowMutate,
_127
useFlowTransaction,
_127
useCurrentFlowUser,
_127
} from "@onflow/kit";
_127
_127
export 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:


_10
npm 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, and useCurrentFlowUser 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.