Web 3.0 frontend stacks in 2023
Tags: web3, blockchain, react
Published: 2023-01-29 12:52:23

---
title: Web 3.0 frontend stacks in 2023
published: true
description:
tags: #web3 #blockchain #react
# cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgc6dcgs9rm0dukfdiwo.png
# Use a ratio of 100:42 for best results.
# published_at: 2023-01-29 12:01 +0000
---

I've been developing web2 development for about 10 years, and I've been developing web3 frontend for 1.5 years.

This is my current stacks/libs for web3 frontend. If you have any other recommendations, please let me know with comment.

## Index

1. [System Architecture](#1-system-architecture)
1. [Stacks, Libs](#2-stacks-libs)
1. [Web3 frontend tips](#3-web3-frontend-tips)
1. [Things that I would try](#4-things-that-i-would-try)

### 1. System architecture
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgc6dcgs9rm0dukfdiwo.png)

### 2. Stacks, Libs

#### Folder structure : [Turborepo](https://turbo.build/repo)

I prefer `monorepo (multiple apps in one repo)` structure. Most of the code is in a common folder under `packages`, and each app has only a minimal page component for routing.

You can easily extend apps in the following cases:

- DApps that has different pages or functions for each chain
- Lite version with limited function
- Middleware or tools using same function

```sh
.
├── apps
│   ├── dapp
│   │   └── pages (or app)  # each app has only pages dir
│   ├── sub-dapp
│   └── ...
├── packages
│   ├── assets
│   │   ├── img
│   │   └── styles
│   ├── config
│   ├── lib
│   │   ├── constants
│   │   │   └── abis  # abi json
│   │   ├── queries  # thegraph queries
│   │   ├── store  # state
│   │   ├── types
│   │   └── utils
│   ├── react-lib
│   │   ├── components  # components
│   │   └── hooks  # hooks
│   └── tsconfig
└── test
```

#### Frontend Framework & hosting : [Next.js](https://nextjs.org/) + [TypeScript](https://www.typescriptlang.org/) + [Vercel](https://vercel.com/)

React is first choice, because of it's many web3 libs.
Vercel is the easiest place deploying Next.js with Turborepo. However, by using vercel's useful features (Edge functions, ISR, etc.), it is more difficult to deploy to IPFS and make dApp fully decentralized.
[Vite](https://vitejs.dev/) or [esbuild](https://esbuild.github.io/) are also suitable for smaller applications and tools.

#### Connecting ethereum : [wagmi](https://wagmi.sh/) + [ethers.js](https://ethers.org/)

`wagmi` is React Hooks built on top of ethers.js. There are many such libs, but I think this is the best. -> [wagmi's Comparison To Other Libraries](https://wagmi.sh/react/comparison)
`wagmi` provide TypeScript types for Ethereum ABIs and this works with `zod` schema. This brings more strict typecheck for dApp. -> [ABIType](https://github.com/wagmi-dev/abitype)
Also wagmi’s hooks are very useful, such as `useContractReads`. It's wrapping `Multicall3` for multiple read-calls.

Write hook pattern example: 
```ts
// react-lib/hooks/useApprove.ts
import { usePrepareContractWrite, useContractWrite, useWaitForTransaction, erc20ABI } from 'wagmi'
....
export default function useApprove(targetAddress: AddressType, owner: AddressType, spender: AddressType) {
  ....
  const args: [AddressType, BigNumber] = useDebounce([spender, BigNumber.from(amount)])
  const enabled: boolean = useDebounce(!!targetAddress && !!owner && !!spender)

  const prepareFn = usePrepareContractWrite({
    address: targetAddress,
    abi: erc20ABI,
    functionName: 'approve',
    args,
    enabled
  })

  const writeFn = useContractWrite({
    ...prepare.config,
    onSuccess(data) {
      ....
    }
  })

  const waitFn = useWaitForTransaction({
    chainId,
    hash: write.data?.hash,
    wait: write.data?.wait
  })

  return {
    prepareFn,
    writeFn,
    waitFn
  }
}
```

```tsx
// in component
import useApprove from 'react-lib/hooks/contracts/useApprove'
....

export default function SomeComponent() {
  const { prepareFn, writeFn, waitFn } = useApprove(targetAddress, account, spenderAddress)

  // handle view with each hook's return value -> https://wagmi.sh/react/hooks/useContractWrite#return-value
  return (
    <div>
      {approve.prepare.isSuccess &&
        <button
          onClick={() => writeFn?.write}
          disabled={writeFn.isLoading || prepareFn.isError}
        >
          Approve
        </button>
      )}
      {writeFn.isLoading && <p>Loading...</p>}
      {writeFn.isSuccess && <p>Tx submitted!</p>}
      {waitFn.isSuccess && <p>Approved!</p>}
    </div>
  )
}
```

#### Indexing : [The Graph](https://thegraph.com/en/) + [urql](https://formidable.com/open-source/urql/)

> The Graph is an indexing protocol for querying networks like Ethereum and IPFS. Anyone can build and publish open APIs, called subgraphs, making data easily accessible.

Since contract calls are slow and can only be called one at a time, an indexing layer is needed to list multiple contract call results and calculate aggregate values.

Aggregation and indexing can be specified in subgraphs triggered by contract events, and the results are stored in thegraph’s db.

On the client side, we can query data graphql format.
`urql` is a lightweight graphql client with a simple caching strategy.

It was a bit difficult to run thegraph node locally and debug subgraph's code. Since [subsquid](https://subsquid.io/) recently added EVM chain support, I will try this next.

```ts
// define query
import { gql } from 'urql'

export const GET_MY_LIQUIDITIES = gql`
  query getMyLiquidities($account: String) {
    ${FRAGMENT}
    user(id: $account) {
      id
      liquidities {
        id
        protocols {
          ...Fragment
        }
      }
    }
  }
`
```
```tsx
// call in component
import { useQuery } from 'urql'
....
export function SomeComponent() {
  ....
  const [{ data, fetching }] = useQuery({
    query: GET_MY_LIQUIDITIES,
    variables: {
      account: account.toLowerCase()
    },
    pause: !account
  })
  ....
}
```

#### UI, CSS : [tailwindcss](https://tailwindcss.com/) + [PostCSS](https://postcss.org/) + [Radix UI](https://www.radix-ui.com/) + [UI components by shadcn](https://github.com/shadcn/ui)

I prefer plain & customizable style-less frameworks. Radix UI has more features than [Headless UI](https://headlessui.com/), but adding style is more difficult. So components by shadcn is good starting point.

I use this [CSS variables with Tailwind CSS setup](https://stackoverflow.com/a/66333605).

Current postcss setup is below.

```js
// postcss.config.js
module.exports = {
  plugins: {
    "postcss-import": {}, // inline importing for external lib's css
    "tailwindcss/nesting": {}, // enable nesting
    tailwindcss: {},
    autoprefixer: {},
  },
};
```

#### State management : [jotai](https://jotai.org/)

`jotai` is a state management library that is easy to use and lightweight.
It can use simply like `useState` + `ContextAPI`, prevent extra-rerender and has many utilities.
Similar libraries are [recoil](https://recoiljs.org/), [zustand](https://zustand-demo.pmnd.rs/), [valtio](https://valtio.pmnd.rs/). You can choose the one you like.

```ts
// packages/store/index.ts
import { atom } from "jotai";

type SessionType = {
  chainId?: number;
  account?: string;
};

export const sessionsAtom = atom<SessionType>({});
```

```tsx
// in component or hook
import { sessionAtom } from 'lib/store'
import { useAtom } from 'jotai'

....
const [session, setSession] = useAtom(sessionAtom)

....
setSession({ chainId: res.chainId, account: res.address })
```

#### Form library : [jotai-form](https://github.com/jotai-labs/jotai-form) & [zod](https://zod.dev)

This setup can be validate form inputs more react-like way. -> [jotai-form with zod example](https://github.com/jotai-labs/jotai-form/blob/main/examples/05_zod/src/index.tsx)
If you aren't using jotai or need more complex validation, you can use [react-hook-form](https://react-hook-form.com/) with zod.

### 3. Web3 frontend tips

#### Handling transactions async

Blockchain response (especially in Mainnet) is very slower than web2's.

After a user submit transaction, there should be appropriate feedback to the user, such as display loading and showing the status of the transaction.

There are some cases where a user actions are not reflected in user's browser because of delays in updating data from thegraph, even though the user's trancsaction has been confirmed. In such case, I use thegraph's data for initial rendering, and overwrite it's value with contract call result later.

In addition, in order to handle cases where a user has left a page or site before the transaction confirmed, a user's uncompleted transactions are stored once in state, persisted in `localStorage`, and held until confirmation.


```tsx
// watching uncompleted transaction
import { useEffect } from 'react'
import { useBlockNumber, useProvider } from 'wagmi'
....

export default function useTxHandler() {
  const { chainId, account } = useWallet()
  const transactions = useTransactions() // get user's transactions from state
  const blockNumber = useBlockNumber({
    chainId,
    scopeKey: 'useTxHandler',
    watch: true
  })
  const provider = useProvider<any>()

  useEffect(() => {
    transactions.map(async (tx: Transaction) => {
      await provider
        .getTransactionReceipt(tx.hash)
        .then(async (transactionReceipt: any) => {
          if (transactionReceipt) {
            .... // update user's transaction status here
            })
          }
        })
        .catch((e: any) => {
          log(`failed to check transaction hash: ${tx.hash}`, e)
        })
    })
  }, [account, chainId, PROVIDER_URL, transactions, blockNumber.data])
}
```

#### Handling BigNumber

ERC20 has `decimals` fields and must be handled with digit awareness.
I wanted to use only one library, but I used both [ether.js's BigNumber](https://docs.ethers.org/v5/api/utils/bignumber/) and [bignumber.js](https://mikemcl.github.io/bignumber.js/) (for display purpose).

#### [Uniswap tokenlist format](https://github.com/Uniswap/token-lists)

> This package includes a JSON schema for token lists, and TypeScript utilities for working with token lists.

OnChain token informations are limited and must be kept somewhere in OffChain in list format.
It is convenient to follow this format published by uniswap when handling.


### 4. Things that I would try

#### [Permit2](https://github.com/Uniswap/permit2)

> Permit2 introduces a low-overhead, next generation token approval/meta-tx system to make token approvals easier, more secure, and more consistent across applications.

Instead of approve each dApp's each contract, once you approve `Permit2`, you can approve allowance to any ERC20 contracts with time-bounded(during the transaction).
This can improve UX (no more approves in my wallet!) & security (Problems caused by large amounts of allowances remaining against each dapp's contract) greatly.

#### [Aztec Connect](https://aztec.network/connect/)

Aztec Connect allows transactions running on Aztec L2 with MainNet's security & liquidity.
There are many advantages such as improved security without redeploying, lower gas fee, etc.

#### [Synpress](https://github.com/Synthetixio/synpress)
> Synpress is e2e testing framework based on Cypress.io and playwright with support for metamask.

I have only written component and library's tests now, but I would like to try E2E testing as well.