Build Modern Web3 dApps on Ethereum With React Native and Viem

In short

This article delves into the creation of modern web3 dApps on Ethereum using React Native and the cutting-edge Viem library.

The core insights:

- Viem is a powerful modern web3 library that offers cutting-edge features as well as exceptional Developer Experience.

- WalletConnect is the actual standard for connecting wallets in Ethereum apps, and provides hundreds of wallets to choose from.

- React Native was often overlooked when building apps in web3, because of the historical focus on web APIs, but this guide showcases how to setup a basic dApp, using modern tools, including necessary polyfills, reading public data from the blockchain and executing operations through a wallet.

Decentralized apps (dApps)

The web3 space is evolving quickly, with new applications, tools, and protocols always emerging. Staking and lending assets for yield, shared social networks, and interchangeable items between totally separate games are just a few examples of the latest trends.

Innovation is constantly ongoing, and so are the tools being used to build these products. If you have ever tried to build decentralized apps (dApps) on the web, you probably have stumbled upon web3.js or ethers.js, two cornerstones of developing web3 apps in JavaScript. They both act as low-level libraries to interact with the blockchain, which means, Ethereum blockchain nodes running either locally on your computer or remotely on a server. The communication is done via the JSON-RPC protocol.

On top of those, many other libraries and frameworks arose using them as a foundation, to the point that now we have a diverse pool of tooling at our disposal, which we can use to build amazing decentralized apps and wallets.

Web3 libraries and React Native

The issue is that the majority of web3 libraries were built with their primary focus on the web, starting from the browser wallet extensions that use <rte-code>window.ethereum<rte-code> injected in the global namespace as its primary form of exposing the API. 

Most tools that don't work well with React Native usually share the same reason: they rely on Node.js or browser APIs. Understandably, they might need that <rte-code>crypto<rte-code> module to perform hashing operations, or converting strings between base64 and binary with <rte-code>btoa<rte-code> and <rte-code>atob<rte-code>.

When talking about React and Ethereum, you may have heard of viem and WalletConnect. They both triumph on the web, but what about React Native? That's what we’ll be exploring in this post.

What is viem?

According to their docs:

viem is a TypeScript interface for Ethereum that provides low-level stateless primitives for interacting with Ethereum.

So <rte-code>viem<rte-code> is a newer alternative to <rte-code>web3.js<rte-code> and <rte-code>ethers.js<rte-code>. It also enables you to talk to the Ethereum RPC servers through their simplified API on top of the JSON-RPC protocol, similarly to the others, so we're able to query or modify data into the blockchain. 

Viem was born to address the issues the latter two suffer from: stability, bundle size, performance and developer experience - issues already well-discussed in the community.

According to BundlePhobia, and checking the latest versions available now, viem weights 62.7kB minified + gzipped, while ethers.js weights 135.9kB and web3.js weights 152.4kB. When importing analog core features, with tree-shaking, <rte-code>viem<rte-code> can get 2 to 5 times smaller than the alternatives.

viem bundle size
Source: https://viem.sh/docs/introduction.html#bundle-size

Regarding performance, <rte-code>viem<rte-code> also claims to beat the alternatives by at least 40 times in core operations like <rte-code>isAddress<rte-code>. Read more in the performance section.

viem performance chart
Source: https://viem.sh/docs/introduction.html#performance

And it has been quickly gaining traction as we can see in this graph from NPM Trends:

graph showing viem npm trends
Source: https://npmtrends.com/ethers-vs-viem-vs-web3

What is WalletConnect?

WalletConnect is an open protocol that allows dApps to interact securely with mobile wallets. It enables users to connect their wallets to dApps by scanning a QR code or clicking on a deep link. Once connected, dApps can send transaction requests to the wallet, and the wallet prompts the user for approval before signing and broadcasting the transactions.

WalletConnect is the actual standard for connecting wallets in Ethereum apps, and with v2 it is expanding to become chain-agnostic. One of its biggest advantages is being supported by dozens (hundreds?) of wallets, hence it is more attractive than integrating several wallet SDKs separately.

Also, the WalletConnect SDKs provide easy-to-use APIs and a plug-and-play UI to choose the wallet that best suits you. It is supported by MetaMask, Rainbow Wallet, Trust Wallet, Coinbase Wallet, Safe Wallet, and many others. The probability that the wallet you're using is supported by them is very high.

walletconnect screen with QR code
Source: https://walletconnect.com/products

In WalletConnect v2.0, a chain-agnostic interface was implemented, and now WalletConnect can support multiple chains besides Ethereum. To understand more about the differences in v2.0, check Pedro Gomes's post on the topic.

Building a dApp in React Native

If you want to check the final code by yourself, here's the repository.

Requirements

  • Expo environment setup (Node.js, Git, Watchman)
  • A Wallet Connect Cloud project ID
  • Expo Go app installed in your smartphone
  • One or more web3 wallets installed in your smartphone (e.g. MetaMask, Rainbow Wallet, Trust Wallet, etc.)

Creating a new Expo app

Start by creating a new Expo app:

Then immediately rename the <rte-code>App.js<rte-code> file to <rte-code>App.tsx<rte-code>. Expo will know we're trying to use TypeScript and will ask to install the dependencies when you try to run the app.

Run the development server, and if everything is working correctly you should have a blank app:

New blank Expo app

Querying data from the blockchain

Now, install <rte-code>viem<rte-code>:

<rte-code>viem<rte-code> needs a simple polyfill for the EcmaScript TextEncoder API, which you can easily install by running the command below. For more information, see Platform Compatibility.

We need to apply the polyfill as the first thing when running our app. So, to keep things organized, let's create a new <rte-code>polyfills.ts<rte-code> file that will hold all our polyfills, in case more are needed later:

And on App.tsx, import the polyfills in the first line:

With <rte-code>viem<rte-code> and the polyfills installed, we're now able to interact with the blockchain data. On <rte-code>App.tsx<rte-code>, create a public client above the App component:

We’ll use the public client to send requests to the RPC server running an Ethereum node. Here we have configured it saying that we're targeting Ethereum <rte-code>mainnet<rte-code>, and will do it through <rte-code>http<rte-code>, but if needed, you could choose to use <rte-code>polygon<rte-code> (or any other chain), as well as communicating through WebSockets or a custom EIP-1193 compatible provider (we’ll see more about this later).

With the public client set up, it's just a matter of calling the right methods and putting in the state so we can visualize.

Create the state variables that are gonna hold our values. <rte-code>viem<rte-code> will return the values as <rte-code>BigInt<rte-code>, so we initialise them with <rte-code>0n<rte-code>:

In an <rte-code>useEffect<rte-code>, fetch the network data on mount:

Then, show them on the screen:

<rte-code>viem<rte-code> responses will always return in wei, which is the smallest unit in Ethereum, and equivalent to 10^-18 ETH. To transform to ETH, we use the <rte-code>formatEther<rte-code> utility.

And ta-da! You should now have your app looking somewhat like this, with real data coming directly from the blockchain.

App querying block number and gas price from the blockchain

Interacting with your wallet

Now, you probably want to connect your wallet, to be able to display your address, sign messages, send transactions and other actions that require wallet permissions.

To support as many wallets as possible (and consequently as many users as possible), we'll use WalletConnect's WalletConnectModal. This is an advanced SDK from WalletConnect for more granular control. At the time of writing, it is currently their only production-ready React Native-compatible SDK, since they just announced the alpha version of Web3Modal SDK for React Native. We’ll cover this one in a future blog post.

WalletConnectModal, as the name says, will open a nice-looking modal UI for the users to select their preferred wallet among several options. Upon selecting, the user should be deep-linked to the wallet app, asking for permission to connect.

Installing WalletConnectModal

Run this to install the WalletConnectModal SDK:

Then, install the other dependencies and polyfills:

Optionally, for iOS, you may want to specify wallets to detect automatically in case the user already has them installed, to show them first. You can do it by adding the wallet schemes under the iOS configuration in <rte-code>app.json<rte-code>:

For more info, or if you're using a different version of Expo, please refer to the WalletConnectModal docs.

Using WalletConnectModal

The setup is done, now let's move to the actual use.

At the top of <rte-code>App.tsx<rte-code>, add your <rte-code>projectId<rte-code> and <rte-code>providerMetadata<rte-code>:

If you don't have a <rte-code>projectId<rte-code> yet, you’ll need to go to WalletConnect Cloud and create an account and project. The project ID will be in the project's settings page.

For <rte-code>providerMetadata<rte-code>, use whatever values best describe your project. The values at the redirect field should represent your app's scheme, for Deep Links, and link, for Universal Links.

To be able to interact with the modal, we’ll call the <rte-code>useWalletConnectModal()<rte-code> hook, at the beginning of the <rte-code>App<rte-code> component:

This provides us with a lot of useful information:

  • <rte-code>open<rte-code>: method to programmatically open the modal
  • <rte-code>isConnected<rte-code>: straightforward, boolean
  • <rte-code>provider<rte-code>: an EIP-1193 compliant provider, a standard for consistency between Ethereum wallets and applications
  • <rte-code>address<rte-code>: the connected wallet's address

The provider is the instance that actually sends requests to the RPC server and listens for events. We won't be using it directly though, we’ll pass it to <rte-code>viem<rte-code> so we can leverage viem's higher-level abstractions.

Now render the modal component, and a Button to open it:

At this point, you should have something like this:

WalletConnectModal opened to select a wallet

Connecting and disconnecting

To connect, it's just a matter of opening modal and choosing the desired wallet, and the connected address will be available through the <rte-code>useWalletConnectModal()<rte-code> hook. To please TypeScript, we’ll re-declare the <rte-code>address<rte-code> variable, since the address returned from the hook is a <rte-code>string<rte-code>, and <rte-code>viem<rte-code> will always consume a more sophisticated <rte-code>0x${string}<rte-code>.

We can update our UI to show the <rte-code>address<rte-code> when <rte-code>isConnected<rte-code> is <rte-code>true<rte-code>, and also add a button to disconnect:

Notice how we can disconnect just by calling <rte-code>provider?.disconnect()<rte-code>.

Now, your app should be working similarly to this:

Connecting a wallet

Setting up listeners

We want to know when the app has connected (<rte-code>connect<rte-code> event) so we can show the address, balance, and chain. We also want to know if the user manually changed chains directly via their wallet (<rte-code>chainChanged<rte-code> event), to show and hide elements, or block and unblock features.

Start by adding a <rte-code>CHAINS<rte-code> constant outside the component, the list of chains that are going to be supported by our dApp. This will allow us to find the chain object easily when switching chains.

Then, we’ll need a second <rte-code>useEffect<rte-code> to register listeners:

  • When the chain changes, we get a <rte-code>chainId<rte-code>. We search for the chain with this id in the <rte-code>CHAINS<rte-code> constant, and set it in the state
  • When the app connects, we extract the <rte-code>chainId<rte-code> from the event, and reuse the same method from when the chain changes, to keep the chain updated. Then, we also get the wallet balance with the <rte-code>getBalance<rte-code> function, and set it in the state

Now, add 2 new state variables to keep track of the current chain and balance :

And update the UI again to show these values:

With the above setup, we already have our UI updated when connecting a wallet or switching chains, nice!

You can test that by manually switching chains in your wallet, and seeing it reflect in the dApp UI:

Manually switching chains in wallet, and reacting in the app UI

Signing a message

Wrapping everything up, let's use our wallet to sign a message, and prove that we're its actual owners.

We’ll create the wallet client and a <rte-code>signature<rte-code> state. The former is similar to the public client we created before, but it has access to the provider and is able to request permissions to the wallet. The latter will hold the signature hash proof.

It's important to memoize the wallet client, since it depends on the provider that comes from the WalletConnect modal hook, and we don't want it being recreated (and messing with our session), when the component re-renders.

Now, let's add a handler that will request the wallet to sign a message when called:

We call <rte-code>viem<rte-code>'s <rte-code>walletClient.signMessage()<rte-code>, passing our address and the message we want to sign. This message will be displayed in the wallet's UI, and wait for the user to confirm. If confirmed, we’ll have a cryptographic signature, which we'll display in the UI just for presentation purposes.

Finally, let's update our UI with a Button to call the <rte-code>onSignMessage<rte-code> handler, and a Text to show the <rte-code>signature<rte-code> proof.

You should end with something that looks like this:

…and works like this:

Signing a message

Summary

To sum things up, the classic libraries are still there and are still going to work. If you're constrained to it, better enjoy it, knowing that you can still build great applications.

But for the modern JavaScript way, there is <rte-code>viem<rte-code>, the cool new kid in town that brings enhanced performance, a smaller footprint, and better DX. Pair it with the WalletConnect solutions and you have a great stack to build any front-end to your smart contracts.

The web3 space is still too young, especially when it comes to React Native.

All the code shown in this guide is available publicly on GitHub.

Latest update:
October 25, 2023

FAQ

No items found.
React Galaxy City
Get our newsletter

By subscribing to the newsletter, you give us consent to use your email address to deliver curated content. We will process your email address until you unsubscribe or otherwise object to the processing of your personal data for marketing purposes. You can unsubscribe or exercise other privacy rights at any time. For details, visit our Privacy Policy.

Callstack astronaut
Download our ebook

I agree to receive electronic communications By checking any of the boxes, you give us consent to use your email address for our direct marketing purposes, including the latest tech & biz updates. We will process your email address and names (if you have entered them into the above form) until you withdraw your consent to the processing of your names, or unsubscribe, or otherwise object to the processing of your personal data for marketing purposes. You can unsubscribe or exercise other privacy rights at any time. For details, visit our Privacy Policy.

By pressing the “Download” button, you give us consent to use your email address to send you a copy of the Ultimate Guide to React Native Optimization.