# Tutorial 4: Build a zkApp UI in the Browser with React

> Guided steps to implement a browser UI that interacts with a smart contract running on the Devnet.

Canonical URL: https://docs.minaprotocol.com/zkapps/tutorials/zkapp-ui-with-react

You're making excellent progress in your zkApp journey:

- In the [Hello World](hello-world) tutorial, you built a basic zkApp smart contract with o1js.
- In [Tutorial 3: Deploy to a Live Network](deploying-to-a-network), you used the `zk` commands to deploy your zkApp.

In this tutorial, you are going to implement a browser UI using `Next.js` that interacts with a smart contract.

## Prerequisites

- Make sure you have the latest version of the zkApp CLI installed:

    ```sh
    $ npm install -g zkapp-cli
    ```

- Ensure your environment meets the [Prerequisites](/zkapps/tutorials#prerequisites) for zkApp Developer Tutorials.

- Use Node.js 22.19.5 or later. o1js 3 requires it.

- The Auro Wallet browser extension wallet that supports interactions with zkApps. See [Install a Wallet](/using-mina/install-a-wallet) and create a MINA account.

This tutorial has been tested with:

- [o1js](https://www.npmjs.com/package/o1js) version `3.0.0`
- [Next.js](https://nextjs.org/) version `14.2.3` and React version `18`
- Node.js versions `22` and `24`

The example project is built and tested in CI. The test runs the web worker functions of the UI against a local blockchain, with real proofs, and does the part of the wallet: it adds the fee payer, signs and sends the transaction. CI does not run the UI in a browser or with Auro Wallet.

## High-Level Overview

In this tutorial, you create a new GitHub repository so you can deploy the UI to GitHub Pages.

You use example code and the zkApp CLI to build an application that:

1. Loads a public key from an extension-based wallet.
1. Checks if the public key has funds and if not, directs the user to the Faucet.
1. Connects to the example zkApp `Add` smart contract that is already deployed on Devnet (or other network) at a fixed address.
1. Implements a button that sends a transaction.
1. Implements a button that requests the latest state of the smart contract.
1. Deploys the zkApp to GitHub Pages.

Like previous tutorials, you use the provided [example files](https://github.com/MinaProtocol/docs2/tree/main/examples/zkapps/04-zkapp-browser-ui) so you can focus on the React implementation itself.

## Create a project

You can have the `zk project` command scaffold the UI for your project.

1. Create or change to the directory where you have write privileges.
1. Create a project by using the `zk project` command:

  ```sh
  $ zk project 04-zkapp-browser-ui
  ```

  To scaffold the UI for your project with the `Next.js` React framework, select `next`:

  ```sh
    ? Create an accompanying UI project too? …
  > next
    svelte
    nuxt
    empty
    none
  ```

1. Select **yes** at the `? Do you want to set up your project for deployment to GitHub Pages? …` prompt.
1. If you are prompted to install the required Next packages, press **y** to proceed.
1. Select **No** at the `? Would you like to use ESLint with this project?` prompt.
1. Select **No** at the `? Would you like to use Tailwind CSS with this project?` prompt.

The project directory `04-zkapp-browser-ui` has two sub-directories:

- `contracts`: The smart contract code
- `ui`: The UI application code

For this tutorial, you run commands from the root of the `04-zkapp-browser-ui/ui` directory. You work in the `ui/app` directory on TypeScript files that contain the UI code.

### Install the dependencies

The zkApp CLI installs the dependencies in each sub-directory.

The zkApp CLI can create the project with an earlier o1js. Set o1js to version 3:

1. In `ui/package.json`, set the `o1js` version in `dependencies` to `^3.0.0`.
1. In `contracts/package.json`, set the `o1js` version in `peerDependencies` to `^3.0.0`.
1. Run `npm install` in the `ui` directory and in the `contracts` directory.

## Create a repository

To interact with a deployed zkApp UI on GitHub pages, you must create a GitHub repository.

Go ahead and create your repository now. For other projects, you can name your GitHub repository anything you want. For this tutorial, use `04-zkapp-browser-ui`.

1. Go to [https://github.com/new](https://github.com/new).
1. For the **Repository name**, enter `04-zkapp-browser-ui`.
1. Optionally, add a description and a README.

Your project repository is ready to use.

### Preparing the project

Start by deleting the default `page.tsx` file that comes with a new project so that you have a clean project to work with.

1. In the `04-zkapp-browser-ui/ui` directory:

  ```sh
  $ rm app/page.tsx
  ```
### Install UI dependencies

This tutorial uses the `comlink` package to integrate web workers into the React application. Comlink simplifies
communication between the main thread and web workers by abstracting the `postMessage` API, allowing you to call functions in the worker as if they were local.

In the `04-zkapp-browser-ui/ui` directory, install `comlink` by running the following command:

```sh
$ npm install comlink
```
To learn more about `comlink` , read the [documentation](https://www.npmjs.com/package/comlink).

### Download helper files

Because o1js code is computationally intensive, it's helpful to use web workers. A web worker handles requests from users to ensure the UI thread isn't blocked during long computations like compiling a smart contract or proving a transaction.

1. Download the helper files from the `examples/zkapps/04-zkapp-browser-ui` directory on GitHub:

    - [zkappWorker.ts](https://github.com/MinaProtocol/docs2/blob/main/examples/zkapps/04-zkapp-browser-ui/ui/app/zkappWorker.ts)
    - [zkappWorkerClient.ts](https://github.com/MinaProtocol/docs2/blob/main/examples/zkapps/04-zkapp-browser-ui/ui/app/zkappWorkerClient.ts)

1. Move the files to your local `04-zkapp-browser-ui/ui/app` directory.

1. Review each helper file to see how they work and how you can extend them for your own zkApp.

    - `zkappWorker.ts` is the web worker code
    - `zkappWorkerClient.ts` is the client code that is run from React to interact with the web worker

### Download the main browser UI logic file

The example project has a completed app. The `page.tsx` file is the entry file for your application and contains the main logic for the browser UI that is ready to deploy to GitHub Pages.

1. Download the [page.tsx](https://github.com/MinaProtocol/docs2/blob/main/examples/zkapps/04-zkapp-browser-ui/ui/app/page.tsx) example file.

1. Move the `page.tsx` file to your local `04-zkapp-browser-ui/ui/app` directory.

## Build the contract

This tutorial uses an `Add` contract with an `update()` method that adds 2 to the on-chain state `num`. The web worker calls `update()`.

Recent versions of the zkApp CLI create a different `Add` contract, which has a `settleState()` method and uses a `ZkProgram` from `src/AddZkProgram.ts`. If your `contracts/src` directory has the file `AddZkProgram.ts`, replace the contract files with the files of the example project:

1. Delete `contracts/src/AddZkProgram.ts`.
1. Replace `contracts/src/Add.ts` with:

   ```ts title="contracts/src/Add.ts"
   import { Field, SmartContract, state, State, method } from 'o1js';

   /**
    * Basic Example
    * See https://docs.minaprotocol.com/zkapps for more info.
    *
    * The Add contract initializes the state variable 'num' to be a Field(1) value by default when deployed.
    * When the 'update' method is called, the Add contract adds Field(2) to its 'num' contract state.
    *
    * This file is safe to delete and replace with your own contract.
    */
   export class Add extends SmartContract {
     @state(Field) num = State<Field>();

     init() {
       super.init();
       this.num.set(Field(1));
     }

     @method async update() {
       const currentState = this.num.getAndRequireEquals();
       const newState = currentState.add(2);
       this.num.set(newState);
     }
   }
   ```

1. Replace `contracts/src/index.ts`, `contracts/src/Add.test.ts` and `contracts/src/interact.ts` with the [example files](https://github.com/MinaProtocol/docs2/tree/main/examples/zkapps/04-zkapp-browser-ui/contracts/src).

To build the contract so that it can be used with UI application, run this command from the `04-zkapp-browser-ui/contracts` directory:

```sh
$ npm run build
```

Outside of this tutorial, the workflow for building your own zkApp is to edit files in the `contracts` folder, rebuild the contract, and then access it from your UI application code.

## Implement the UI

The UI application has several components: the React page itself and the code that uses o1js.

### Setup web workers

The web worker code resides in the `04-zkapp-browser-ui/ui/app/zkappWorker.ts` file. Here, you define the functions that will be executed in the worker thread.

#### Defining State

The state object holds references to the `Add` contract class, the zkApp instance, and the transaction.

```ts title="ui/app/zkappWorker.ts"
const state = {
  AddInstance: null as null | typeof Add,
  zkappInstance: null as null | Add,
  transaction: null as null | Transaction,
};
```

##### Defining functions that will run in the worker thread

These functions perform tasks such as setting up the network instance, loading and compiling the smart contract, fetching accounts, interacting with the smart contract, and handling transactions. The functions will run in the web worker thread to ensure the UI thread is not blocked during long computations.

```ts title="ui/app/zkappWorker.ts"
export const api = {
  async setActiveInstanceToDevnet() {
    const Network = Mina.Network('https://devnet-plain-1.gcp.o1test.net/graphql');
    console.log('Devnet network instance configured');
    Mina.setActiveInstance(Network);
  },
  async loadContract() {
    const { Add } = await import('../../contracts/build/src/Add.js');
    state.AddInstance = Add;
  },
  async compileContract() {
    await state.AddInstance!.compile();
  },
  async fetchAccount(publicKey58: string) {
    const publicKey = PublicKey.fromBase58(publicKey58);
    return fetchAccount({ publicKey });
  },
  async initZkappInstance(publicKey58: string) {
    const publicKey = PublicKey.fromBase58(publicKey58);
    state.zkappInstance = new state.AddInstance!(publicKey);
  },
  async getNum() {
    const currentNum = await state.zkappInstance!.num.get();
    return JSON.stringify(currentNum.toJSON());
  },
  async createUpdateTransaction() {
    state.transaction = await Mina.transaction(async () => {
      await state.zkappInstance!.update();
    });
  },
  async proveUpdateTransaction() {
    await state.transaction!.prove();
  },
  async getTransactionJSON() {
    return state.transaction!.toJSON();
  },
};
```

#### Expose functions to the main thread with `comlink`

```ts title="ui/app/zkappWorker.ts"
// Expose the API to be used by the main thread
Comlink.expose(api);
```

#### Creating the worker client

- The web worker client code resides in the `04-zkapp-browser-ui/ui/app/zkappWorkerClient.ts` file. Here you create a client in the main thread that interacts with the web worker.

- `worker` is a reference to the web worker instance and `remoteApi` is a reference to a proxy object that provides typesafe access to the worker's API methods.

```ts title="ui/app/zkappWorkerClient.ts"
export default class ZkappWorkerClient {
  worker: Worker;
  // Proxy to interact with the worker's methods as if they were local
  remoteApi: Comlink.Remote<typeof import('./zkappWorker').api>;
```

In the constructor, create a new `Worker` instance from the `zkappWorker.ts` file. With `Comlink.wrap`, create a proxy object `remoteApi` that provides typesafe access to the worker's API methods.

```ts title="ui/app/zkappWorkerClient.ts"
constructor() {
  // Initialize the worker from the zkappWorker module
  this.worker = new Worker(new URL('./zkappWorker.ts', import.meta.url), { type: 'module' });
  // Wrap the worker with Comlink to enable direct method invocation
  this.remoteApi = Comlink.wrap(this.worker);
}
```

Define methods in the `ZkappWorkerClient` class that call the corresponding method on `remoteApi`, effectively forwarding the calls to the worker.

```ts title="ui/app/zkappWorkerClient.ts"
async setActiveInstanceToDevnet() {
  return this.remoteApi.setActiveInstanceToDevnet();
}

async loadContract() {
  return this.remoteApi.loadContract();
}

async compileContract() {
  return this.remoteApi.compileContract();
}

async fetchAccount(publicKeyBase58: string) {
  return this.remoteApi.fetchAccount(publicKeyBase58);
}

async initZkappInstance(publicKeyBase58: string) {
  return this.remoteApi.initZkappInstance(publicKeyBase58);
}

async getNum(): Promise<Field> {
  const result = await this.remoteApi.getNum();
  return Field.fromJSON(JSON.parse(result as string));
}

async createUpdateTransaction() {
  return this.remoteApi.createUpdateTransaction();
}

async proveUpdateTransaction() {
  return this.remoteApi.proveUpdateTransaction();
}

async getTransactionJSON() {
  return this.remoteApi.getTransactionJSON();
}
```

### Environment configuration

- In `04-zkapp-browser-ui/ui/app/page.tsx`

  ```tsx
  let transactionFee = 0.1;
  const ZKAPP_ADDRESS = 'B62qpXPvmKDf4SaFJynPsT6DyvuxMS9H1pT4TGonDT26m599m7dS9gP';
  ```

  `ZKAPP_ADDRESS` is the public key of an `Add` contract on Devnet. The UI reads its state.

  :::caution

  The `Add` contract at this address was deployed with an earlier version of o1js. Its verification key is not the verification key that o1js 3.0.0 compiles for `Add`, so Devnet rejects a transaction that you prove with o1js 3.0.0. To send transactions, [deploy](deploying-to-a-network) the `Add` contract in the `contracts` folder yourself, and replace the value of `ZKAPP_ADDRESS` with the public key of your zkApp.

  :::

- In `04-zkapp-browser-ui/ui/app/zkappWorker.ts`

  ```ts
  async setActiveInstanceToDevnet() {
    const Network = Mina.Network('https://devnet-plain-1.gcp.o1test.net/graphql');
    console.log('Devnet network instance configured');
    Mina.setActiveInstance(Network);
  },
  ```

  Depending on the network you are going to work with you might want to consider changing the GraphQL endpoint in the `setActiveInstanceToDevnet` function.
  Mind the supported networks by `Auro Wallet` though.

:::info

In this example, the `o1js` code is included in a client component and executed on the client side using an effect after the page loads. If you're integrating `o1js` within a server component, be aware that Next.js's caching mechanism might cause `o1js` to return outdated data. To prevent this, you can disable caching by adding `export const revalidate = 0;` to your component. For more details, refer to the [Next.js 14 caching documentation](https://nextjs.org/docs/14/app/building-your-application/caching#opting-out-2).

:::

### Add state

These `04-zkapp-browser-ui/ui/app/page.tsx` statements create mutable state that you can reference in the UI. The state updates as the application runs:

```tsx
const [zkappWorkerClient, setZkappWorkerClient] = useState<null | ZkappWorkerClient>(null);
const [hasWallet, setHasWallet] = useState<null | boolean>(null);
const [hasBeenSetup, setHasBeenSetup] = useState(false);
const [accountExists, setAccountExists] = useState(false);
const [currentNum, setCurrentNum] = useState<null | Field>(null);
const [publicKeyBase58, setPublicKeyBase58] = useState('');
const [creatingTransaction, setCreatingTransaction] = useState(false);
const [displayText, setDisplayText] = useState('');
const [transactionlink, setTransactionLink] = useState('');
```

To learn more about `useState` hooks, see [built-in React hooks](https://react.dev/reference/react/hooks#state-hooks) in the React API reference documentation.

### zkApp setting up

This `04-zkapp-browser-ui/ui/app/page.tsx` code adds a functions to set up zkApp:

- The Boolean `hasBeenSetup` ensures that the react feature `useEffect` is run only once.
To learn more about `useEffect` hooks, see [useEffect](https://react.dev/reference/react/useEffect) in the React API reference documentation.

- This code also sets up your web worker client that interacts with the web worker running o1js code to ensure the computationally heavy o1js code doesn't block the UI thread.

#### Load web worker and setup Mina active instance

```tsx
displayStep('Loading web worker...')
const zkappWorkerClient = new ZkappWorkerClient();
setZkappWorkerClient(zkappWorkerClient);
await new Promise((resolve) => setTimeout(resolve, 5000));
displayStep('Done loading web worker')

await zkappWorkerClient.setActiveInstanceToDevnet();
```

#### Connect Auro Wallet and setup fee payer account

```tsx
const mina = (window as any).mina;
if (mina == null) {
  setHasWallet(false);
  displayStep('Wallet not found.');
  return;
}

const publicKeyBase58: string = (await mina.requestAccounts())[0];
setPublicKeyBase58(publicKeyBase58);
displayStep(`Using key:${publicKeyBase58}`);

displayStep('Checking if fee payer account exists...');
const res = await zkappWorkerClient.fetchAccount(
publicKeyBase58,
);
const accountExists = res.error === null;
setAccountExists(accountExists);
```

#### Import the contract code, instantiate zkApp instance, compile the contract and fetch zkApp state

```tsx
await zkappWorkerClient.loadContract();

displayStep('Compiling zkApp...');
await zkappWorkerClient.compileContract();
displayStep('zkApp compiled');

await zkappWorkerClient.initZkappInstance(ZKAPP_ADDRESS);

displayStep('Getting zkApp state...');
await zkappWorkerClient.fetchAccount(ZKAPP_ADDRESS);
const currentNum = await zkappWorkerClient.getNum();
setCurrentNum(currentNum);
console.log(`Current state in zkApp: ${currentNum}`);
```

#### Update the state of the React application

```tsx
setHasBeenSetup(true);
setHasWallet(true);
setDisplayText('');
```

### Run the React app

Execute the following commands being within the `04-zkapp-browser-ui/ui/` directory.

1. To start the development server and serve your UI application at the URL `localhost:3000`:

  ```sh
  $ npm run dev
  ```

  You can also change the default port by starting the dev server with the `--port` CLI argument. For example, to start the dev server on port `8001`, run:

  ```sh
  $ npm run dev -- --port 8001
  ```

  The zkApp UI in the web browser shows the current state of the zkApp and has buttons to send a transaction and get the latest zkApps on-chain state.

  Your browser refreshes automatically when you update the source code.

1. If prompted, request the funds from the Testnet Faucet service to fund your fee payer account.
1. In a second terminal window, run:

  ```sh
  $ npm run ts-watch
  ```

  This command starts the installed TypeScript compiler (`tsc`) with `--watch` parameter, with the ability to react to compilation status.

### Wait for the fee payer account to be funded

Now that the UI setup is finished, a new useEffect waits for the fee payer account to be funded if it didn't before by checking the account presence in ledger.

Don't forget that if the account has been newly created, it must be funded from the Faucet.

```tsx
useEffect(() => {
  const checkAccountExists = async () => {
    if (hasBeenSetup && !accountExists) {
      try { 
        for (;;) {
          displayStep('Checking if fee payer account exists...');

          const res = await zkappWorkerClient!.fetchAccount(publicKeyBase58);
          const accountExists = res.error == null;
          if (accountExists) {
            break;
          }
          await new Promise((resolve) => setTimeout(resolve, 5000));
        } 
      } catch (error: any) {
        displayStep(`Error checking account: ${error.message}`);
      }

    }
    setAccountExists(true);
  };

  checkAccountExists();
}, [zkappWorkerClient, hasBeenSetup, accountExists]);
```

### Let UI buttons do some useful work

These functions run when the user presses the buttons. While a transaction is in progress, both buttons are disabled.

```tsx
const onSendTransaction = async () => {
  setCreatingTransaction(true);
  try {
    displayStep('Creating a transaction...');

    console.log('publicKeyBase58 sending to worker', publicKeyBase58);
    await zkappWorkerClient!.fetchAccount(publicKeyBase58);

    await zkappWorkerClient!.createUpdateTransaction();

    displayStep('Creating proof...');
    await zkappWorkerClient!.proveUpdateTransaction();

    displayStep('Getting transaction JSON...');
    const transactionJSON = await zkappWorkerClient!.getTransactionJSON();

    displayStep('Requesting send transaction...');
    const { hash } = await (window as any).mina.sendTransaction({
      transaction: transactionJSON,
      feePayer: {
        fee: transactionFee,
        memo: '',
      },
    });

    const transactionLink = `https://minascan.io/devnet/tx/${hash}`;
    setTransactionLink(transactionLink);
    setDisplayText(transactionLink);
  } catch (error: any) {
    displayStep(`Error sending transaction: ${error.message}`);
  } finally {
    setCreatingTransaction(false);
  }
};

// -------------------------------------------------------
// Refresh the current state

const onRefreshCurrentNum = async () => {
  try {
    displayStep('Getting zkApp state...');
    await zkappWorkerClient!.fetchAccount(ZKAPP_ADDRESS);
    const currentNum = await zkappWorkerClient!.getNum();
    setCurrentNum(currentNum);
    console.log(`Current state in zkApp: ${currentNum}`);
    setDisplayText('');
  } catch (error: any) {
    displayStep(`Error refreshing state: ${error.message}`);
  }
};
```

### Take care of the page markup

```tsx
let auroLinkElem;
if (hasWallet === false) {
  const auroLink = 'https://www.aurowallet.com/';
  auroLinkElem = (
    <div>
      Could not find a wallet.{' '}
      <a href={auroLink} target="_blank" rel="noreferrer">
        Install Auro wallet here
      </a>
    </div>
  );
}

const stepDisplay = transactionlink ? (
  <a
    href={transactionlink}
    target="_blank"
    rel="noreferrer"
    style={{ textDecoration: 'underline' }}
  >
    View transaction
  </a>
) : (
  displayText
);

let setup = (
  <div
    className={styles.start}
    style={{ fontWeight: 'bold', fontSize: '1.5rem', paddingBottom: '5rem' }}
  >
    {stepDisplay}
    {auroLinkElem}
  </div>
);

let accountDoesNotExist;
if (hasBeenSetup && !accountExists) {
  const faucetLink =
    `https://faucet.minaprotocol.com/?address=${publicKeyBase58}`;
  accountDoesNotExist = (
    <div>
      <span style={{ paddingRight: '1rem' }}>Account does not exist.</span>
      <a href={faucetLink} target="_blank" rel="noreferrer">
        Visit the faucet to fund this fee payer account
      </a>
    </div>
  );
}

let mainContent;
if (hasBeenSetup && accountExists) {
  mainContent = (
    <div style={{ justifyContent: 'center', alignItems: 'center' }}>
      <div className={styles.center} style={{ padding: 0 }}>
        Current state in zkApp: {currentNum?.toString()}{' '}
      </div>
      <button
        className={styles.card}
        onClick={onSendTransaction}
        disabled={creatingTransaction}
      >
        Send Transaction
      </button>
      <button
        className={styles.card}
        onClick={onRefreshCurrentNum}
        disabled={creatingTransaction}
      >
        Get Latest State
      </button>
    </div>
  );
}

return (
  <GradientBG>
    <div className={styles.main} style={{ padding: 0 }}>
      <div className={styles.center} style={{ padding: 0 }}>
        {setup}
        {accountDoesNotExist}
        {mainContent}
      </div>
    </div>
  </GradientBG>
);
```

The UI has three sections:

- `setup` lets the user know when the zkApp has finished loading.
- `accountDoesNotExist` gives the user a link to the Faucet if their account hasn't been funded.
- `mainContent` shows the current zkApp on-chain state and buttons to let users interact with zkApp.
  The buttons allow the user to create transaction in order to update on-chain zkApp state and refresh the current on-chain zkApp state.

That's it for the code review!

If you've been using `npm run dev`, you can now interact with the UI application on [`localhost:3000`](http://localhost:3000).

## Deploying the application to GitHub Pages

Before you can deploy your project to GitHub Pages, you must push it to a new GitHub repository that you've created at the beginning of this tutorial.

- The GitHub repo must have the same name as the project name.
- In this tutorial, the project name is `04-zkapp-browser-ui`.
- The `zk project` command created the correct project name strings in the `ui/next.config.mjs`, `ui/app/reactCOIServiceWorker.tsx` and `ui/ghp-postbuild.js` files.

To deploy the UI:

1. Change to the `04-zkapp-browser-ui/ui/` directory.
1. Run the `deploy` script by executing the following command:

  ```sh
  npm run deploy
  ```

Scripts defined in the `04-zkapp-browser-ui/ui/package.json` file do the work to build your application and publish it to the GitHub Pages.

After the command completion your zkApp UI will be available at:

```
https://<username>.github.io/04-zkapp-browser-ui/
```

where `<username>` is your GitHub username.

## Conclusion

Congratulations! You built a React UI for your zkApp that allows users to interact with deployed smart contract.

You can build UI for your zkApps using other frameworks like `SvelteKit` and `NuxtJS`.

You are ready to continue with [Tutorial 5: Common Types and Functions](common-types-and-functions) to learn about different o1js types you can use in your zkApps.
