dApp Frontend Development with Vue.js

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
dApp Frontend Development with Vue.js
Medium
~1-2 weeks
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

dApp Frontend Development with Vue.js

Vue.js in Web3 is not second-class compared to React. The issue is that most Web3 libraries were written React-first: wagmi, RainbowKit, ConnectKit — all React hooks. For Vue, you need a different stack, and it exists: wagmi/core (framework-agnostic) + vue-query + web3modal. Or ethers.js directly with Vue 3 Composition API. Let's explore both approaches.

Stack and Architecture

Option 1: wagmi/core + vue-query

@wagmi/core is the headless version of wagmi without React attachment. All actions (connect, readContract, writeContract, watchAccount) work as regular functions. vue-query (TanStack Query for Vue) handles caching and reactivity.

// wagmi.config.ts
import { createConfig, http } from '@wagmi/core'
import { mainnet, polygon } from '@wagmi/core/chains'
import { walletConnect, injected } from '@wagmi/connectors'

export const config = createConfig({
  chains: [mainnet, polygon],
  connectors: [
    injected(), // MetaMask and other EIP-1193
    walletConnect({ projectId: import.meta.env.VITE_WC_PROJECT_ID }),
  ],
  transports: {
    [mainnet.id]: http(),
    [polygon.id]: http(),
  },
})

Composable for account management:

// composables/useWallet.ts
import { ref, computed, watchEffect } from 'vue'
import { connect, disconnect, getAccount, watchAccount } from '@wagmi/core'
import { config } from '@/wagmi.config'

export function useWallet() {
  const account = ref(getAccount(config))

  const unwatch = watchAccount(config, {
    onChange(data) { account.value = data }
  })

  onUnmounted(() => unwatch())

  return {
    address: computed(() => account.value.address),
    isConnected: computed(() => account.value.isConnected),
    chainId: computed(() => account.value.chainId),
    connect: (connector) => connect(config, { connector }),
    disconnect: () => disconnect(config),
  }
}

Option 2: ethers.js + Vue 3 Composition API

If the project already uses ethers.js or your team knows ethers better:

// composables/useEthers.ts
import { ref, shallowRef } from 'vue'
import { BrowserProvider, JsonRpcSigner } from 'ethers'

export function useEthers() {
  const provider = shallowRef<BrowserProvider | null>(null)
  const signer = shallowRef<JsonRpcSigner | null>(null)
  const address = ref<string | null>(null)

  async function connectWallet() {
    if (!window.ethereum) throw new Error('No wallet detected')
    const _provider = new BrowserProvider(window.ethereum)
    const _signer = await _provider.getSigner()
    provider.value = _provider
    signer.value = _signer
    address.value = await _signer.getAddress()

    window.ethereum.on('accountsChanged', (accounts: string[]) => {
      address.value = accounts[0] ?? null
    })
    window.ethereum.on('chainChanged', () => window.location.reload())
  }

  return { provider, signer, address, connectWallet }
}

shallowRef for provider and signer is important. Deep reactivity of Vue on ethers.js objects causes performance issues and strange bugs.

Reading Contract Data

With vue-query caching and refetch work transparently:

// composables/useTokenBalance.ts
import { useQuery } from '@tanstack/vue-query'
import { readContract } from '@wagmi/core'
import { erc20Abi } from 'viem'
import { config } from '@/wagmi.config'

export function useTokenBalance(tokenAddress: Ref<`0x${string}`>, owner: Ref<`0x${string}` | undefined>) {
  return useQuery({
    queryKey: computed(() => ['balance', tokenAddress.value, owner.value]),
    queryFn: () => readContract(config, {
      address: tokenAddress.value,
      abi: erc20Abi,
      functionName: 'balanceOf',
      args: [owner.value!],
    }),
    enabled: computed(() => !!owner.value),
    staleTime: 10_000, // 10 second cache
  })
}

Writing to Contract and Transaction State

Mutations via vue-query useMutation, transaction state via waitForTransactionReceipt:

import { useMutation } from '@tanstack/vue-query'
import { writeContract, waitForTransactionReceipt } from '@wagmi/core'

export function useApprove(tokenAddress: Ref<`0x${string}`>) {
  return useMutation({
    mutationFn: async ({ spender, amount }: { spender: `0x${string}`, amount: bigint }) => {
      const hash = await writeContract(config, {
        address: tokenAddress.value,
        abi: erc20Abi,
        functionName: 'approve',
        args: [spender, amount],
      })
      await waitForTransactionReceipt(config, { hash })
      return hash
    },
  })
}

In component:

<script setup>
const { mutate: approve, isPending, isSuccess, error } = useApprove(tokenAddress)
</script>

<template>
  <button @click="approve({ spender, amount })" :disabled="isPending">
    {{ isPending ? 'Confirm in wallet...' : 'Approve' }}
  </button>
  <p v-if="error">{{ error.shortMessage }}</p>
</template>

Web3Modal for Multi-wallet Connection

WalletConnect's Web3Modal works with Vue through @web3modal/wagmi/vue:

import { createWeb3Modal } from '@web3modal/wagmi/vue'

createWeb3Modal({
  wagmiConfig: config,
  projectId: import.meta.env.VITE_WC_PROJECT_ID,
  enableAnalytics: false,
})

After that in any component:

<w3m-button />

Ready UI with support for MetaMask, WalletConnect, Coinbase Wallet, Injected and 300+ wallets. Customization via CSS variables.

State Management: Pinia for Global Web3 State

For data needed in multiple unrelated components (balance, allowances), Pinia store:

// stores/web3.ts
import { defineStore } from 'pinia'
import { useWallet } from '@/composables/useWallet'

export const useWeb3Store = defineStore('web3', () => {
  const wallet = useWallet()
  // computed getters, actions for batch operations
  return { ...wallet }
})

Build: Vite + @vitejs/plugin-vue

Node polyfills for libraries expecting Node.js environment (some ethers.js parts):

// vite.config.ts
import { nodePolyfills } from 'vite-plugin-node-polyfills'

export default defineConfig({
  plugins: [vue(), nodePolyfills({ include: ['buffer', 'stream', 'util'] })],
  resolve: {
    alias: { '@': '/src' }
  }
})

Without polyfills Buffer is not defined or process is not defined — classic errors on first Web3 project run on Vite.