# Creating a Staking Feature with React and TypeScript

By [starrdev](https://paragraph.com/@starrdev) · 2023-10-14

---

Table of contents:
------------------

1.  Intro
    
2.  Video Demo
    
3.  Tech stack used
    
4.  The Staking flow - how it works
    
5.  Components and functions used
    
6.  How to build a modal with React
    
7.  Zustand store
    
8.  Wagmi hooks
    
9.  Controlling variables
    
10.  Transaction Tracker
    
11.  Action Buttons
    
12.  Summary
    
13.  Conclusion
    

Intro
-----

This article is a technical write-up about the token staking feature for the AirSwap Member Dashboard app. The majority of my time working on this app was spent on this feature.

Users can stake tokens into the AirSwap `Staking` [smart contract](https://github.com/airswap/airswap-protocols/blob/develop/source/staking/contracts/Staking.sol), which gives them access to voting on proposals, then claim rewards from the AirSwap `Pool` [smart contract](https://github.com/airswap/airswap-protocols/blob/develop/source/pool/contracts/Pool.sol). This article will detail the logic on how I built the staking feature.

Video demo
----------

[https://vimeo.com/874258804](https://vimeo.com/874258804)

[https://vimeo.com/manage/videos/877284150](https://vimeo.com/manage/videos/877284150)

Tech stack used
---------------

The app is built using React, TypeScript, and TailwindCSS. All smart contract interactions use a library called Wagmi. Here’s a brief overview:

*   **React** is a popular JavaScript library for building user interfaces. [Docs](https://react.dev/).
    
*   **TypeScript** is a superset of JavaScript that adds static typing to the language. [Docs](https://www.typescriptlang.org/).
    
*   **TailwindCSS** is a CSS framework that allows you to write CSS classes directly into HTML, or JSX in the case of React. Some love it, some hate. I’m among those who love it. [Docs](https://tailwindcss.com/docs/installation).
    
*   **Wagmi**: Wagmi is a collection of React hooks that make it simple to interact with Ethereum and EVM blockchains. [Docs](https://wagmi.sh/).
    

The Staking flow - how it works
-------------------------------

![The screen you'll see after successfully approving AST](https://storage.googleapis.com/papyrus_images/0d0c74cfd27ed2e0274e691409463631ae28b3194324c0aa4213c84fb98bf02d.png)

The screen you'll see after successfully approving AST

If you hold AirSwap Token, ([AST](https://dexscreener.com/ethereum/0x117439f6fdde9a09d28eb78759cd5c852a8653f0)), you can use this app to stake your tokens. Here’s the flow of how staking works:

1.  **Approve**: To stake tokens, first you have to approve the AirSwap Staking contract to spend your AST tokens. This is done by calling the `approve` function, and is a feature in most smart contracts.
    
2.  **Stake**: After you’ve approved the spending of your token, you can call the `stake` function. When you stake your tokens, you’ll receive sAST tokens in exchange for locking up your AST tokens in the smart contract. sAST is kind of like an IOU for AST tokens that you receive from the smart contract. It’s an IOU, but your sAST balance is also used to calculate your voting power.
    
3.  **Unstake**: Calling the `unstake` function lets you unlock your tokens from the smart contract. Note that when you stake `AST` tokens, your tokens unlock linearly over time.
    

Components and functions used
-----------------------------

This section covers various components and functions in the Staking feature. Every function won’t be described here, but feel free to [view all the code on GitHub](https://github.com/airswap/airswap-voter-rewards/tree/main/src/features/staking) if you’re curious.

How to build a modal with React
-------------------------------

![The modal is the box in the center of the screen. Clicking the X in the upper right corner, or pressing the "escape" key will close the modal.](https://storage.googleapis.com/papyrus_images/e843f9ff82cded18dde1dfac33b6628e99347d774d8a10764d38c89a530e8a9d.png)

The modal is the box in the center of the screen. Clicking the X in the upper right corner, or pressing the "escape" key will close the modal.

The `Modal` component in the app utilizes the HTML `dialog` element. The `dialog` element is a modern, elegant way to create a modal. It comes with built-in JavaScript methods, such as `showModal` and `close`.

To create a modal, use the JSX tag `<dialog />`, and pass it a ref. For the ref, we can utilize the `useRef` hook. `useRef` is a React hook that persists a value between renders. Since the value persists, it means that our modal can remain open (or closed) when we want it to.

**Here’s a code sample of the** `Modal` **component:**

    import { useKeyboardEvent } from "@react-hookz/web";
    import { useEffect, useRef } from "react";
    
    export const Modal = ({
      // omitted code
      isClosable = true,
      onCloseRequest,
    }: {
      // omitted code
      isClosable?: boolean;
      onCloseRequest: () => void;
    }) => {
      const modalRef = useRef<HTMLDialogElement>(null);
    
      useKeyboardEvent("Escape", () => {
        if (!isClosable) return;
        onCloseRequest && onCloseRequest();
        modalRef.current?.close();
      });
    
      useEffect(() => {
        if (modalRef.current && !modalRef.current.hasAttribute("open")) {
          modalRef.current.showModal();
        }
      }, [modalRef]);
    
      return (
        <dialog ref={modalRef} >
        // omitted code
        </dialog>
      );
    };
    

In the code above, first we create a ref object called `modalRef`, which gets passed into the `dialog`. We also have a hook called `useKeyboardEvent`, which gets returned from the react-hookz library. `useKeyboardEvent` returns a callback function that closes the modal when the user presses the “escape” key.

Note that `close` is a method which gets called on `modalRef`. Since the `dialog` node has a ref of `modalRef`, the ref object now can access the JavaScript functions that are associated with the HTML `dialog` element, and pass it along to the `dialog`.

![Clicking the blue “STAKING” button in the header opens the staking modal.](https://storage.googleapis.com/papyrus_images/8f9fabec9a8e4ab5c011c241307fce65cd797c561039a6015034a5276a64a510.png)

Clicking the blue “STAKING” button in the header opens the staking modal.

The `useEffect` hook checks whether the `dialog` is open. The `Modal` component opens when a user clicks on a button in another component, but I’ve omitted that code from this blog post. Opening the `Modal` component would give it an attribute of `open`.

![Above is a screenshot of HTML if you log console.log(modalRef.current) in dev tools. Note how dialog has the attribute open.](https://storage.googleapis.com/papyrus_images/6e933e8b9a570826137bc7679904f8b8c00c7e5ec3e5e212be9e01e4c49e05f6.png)

Above is a screenshot of HTML if you log console.log(modalRef.current) in dev tools. Note how dialog has the attribute open.

**Zustand store**
-----------------

Zustand is a lightweight alternative to Redux. We chose Zustand over Redux because the app is relatively small in size. Redux would’ve been overkill for our needs on this app.

We store values in the Zustand store so these values persist across component re-renders and various stages of the staking cycle. It’s possible to use React “prop-drilling” to pass values from parent to children components, but using Zustand produces cleaner code.

Another cool feature of Zustand is that it comes with `persist` middleware. This middleware makes it easy to “persist” data into local storage. This middleware isn’t used for the staking modal, but some other features in the app utilize it.

**Wagmi hooks**
---------------

![](https://storage.googleapis.com/papyrus_images/1bb48d9826084afa84087f1a148cc6e364a5ee01bbcb78a1658d579d0f7cba0a.png)

In the Staking feature, there are main 4 custom hooks which call Wagmi hooks:

1.  **useApproveAst.ts** - calls smart contract “write” function to approve the spending of `AST` token
    
2.  **useAstAllowance.ts** - calls a smart contract “read” function to return the amount of `AST` tokens held in a connected wallet
    
3.  **useStakeAst.ts** - calls smart contract “write” function to stake AST
    
4.  **useUnstakeSast.ts** - calls smart contract “write” function to unstake AST
    

These hooks are mostly similar, so for brevity I’ll only cover `useStakeAst` in detail.

### **Staking hook -** `useStakeAst`

![](https://storage.googleapis.com/papyrus_images/adc10d6f9df1d338c0b64028a7cc55dcea6f621d26b98e4a0c1b7adde7d08249.png)

This hook returns a function that users can use to stake tokens. It calls 3 Wagmi hooks:

1.  `usePrepareContractWrite`. This hook prepares an object called `config`. This object later gets passed into `useContractWrite`, which is explained below. `usePrepareContractWrite` accepts several arguments:
    
    1.  **Address** - the smart contract address of the contract you want to use.
        
    2.  **ABI** - an interface of the smart contract.
        
    3.  **Function** - the function on the ABI you want to call. In our case, `stake`.
        
    4.  **Arguments** - an optional array of arguments to be passed into the function want to call.
        
    5.  **Enabled** - an optional boolean value. If `false`, the function will not run. Using this can optimize the performance of the app. For this purpose, variable called `canStake` was used. `canStake` is a boolean value that checks that the transaction type is “stake”, that `needsApproval` is false, and that the input the user entered is valid.
        
2.  `useContractWrite` - This hook is used to call smart contract functions. It returns several values that were utilized:
    
    1.  `write` - this is the function that writes to the blockchain when it gets called. When a user clicks the “stake” button, this “write” function gets called.
        
    2.  `data` - this object returns data about the transaction after `write` gets called. It contains useful info such as the transaction hash.
        
    3.  `reset`\- After a transaction completes, the status of the transaction will persist until it changes, or the user refreshes the page. The code will look like this: `status === 'success'`. Calling `reset` will reset the transaction status and prevent UX pitfalls.
        
3.  `useWaitForTransaction` - we use this hook primarily to get the status of a transaction, and its transaction hash.
    
    *   `status` can either be: “error”, “idle”, “success”, or “loading. These values change during the course of a transaction’s lifetime.
        
    *   `data` is a return object that contains a transaction hash. We use the transaction has to link users to an etherscan link with their transaction data.
        

![Clicking on the "View on Etherscan" link opens an Etherscan transaction link](https://storage.googleapis.com/papyrus_images/2e936970f80f298f71bb449717b729bbcb758ae1a192d87e271fefd6f16371f8.png)

Clicking on the "View on Etherscan" link opens an Etherscan transaction link

**Controlling variables**
-------------------------

![The transaction tracker conditionally renders a variety of things. Much of it is determined by the variables below.](https://storage.googleapis.com/papyrus_images/52d66c59a3425defd7761fa91a5c1ce78831f098bbf433ef8b9a6a63ab0dcbfd.png)

The transaction tracker conditionally renders a variety of things. Much of it is determined by the variables below.

Several variables used in the main `StakingModal` component are used to control the flow of certain hooks. For example, the following boolean variables: `needsApproval`, `canStake`, and `canUnstake`. If `needsApproval` is true, the hook `useApproveAst` will be enabled, and the hook `useStakeAst` will be disabled. For `canStake` to be true, `needsApproval` must be false.

Here are a few code snippets with explanations below:

    const needsApproval =
        txType === TxType.STAKE &&
        Number(astAllowance) < Number(stakingAmountFormatted) * 10 ** 4 &&
        validNumberInput;
    
    const canStake =
      txType === TxType.STAKE && !needsApproval && validNumberInput;
    
    const {
        writeAsync: approveAst,
        data: dataApproveAst,
        reset: resetApproveAst,
        isLoading: approvalAwaitingSignature,
      } = useApproveAst({
        stakingAmountFormatted: Number(stakingAmountFormatted) || 0,
        enabled: needsApproval,
      });
    

**Check if a user can approve tokens or not:**

`needsApproval` is a boolean value with a few checks. The user must toggle the “stake” option, the user’s allowance must be less than the amount they entered to stake, and the number inputted to the form must be valid.

**Transaction Type**

Let’s look at the following code: `txType === TxType.STAKE`. The `txType` variable is stored in the Zustand store. (Code is not shown above). The value of `txType` changes when you click on the STAKE/UNSTAKE toggle.

![Clicking the "STAKE" option will toggle txType so it's equal to txType.STAKE](https://storage.googleapis.com/papyrus_images/17c0afeddc2a1f8abd9d54d6c2adbcc3901f6334a9d7b791693e6fc48436b5cf.png)

Clicking the "STAKE" option will toggle txType so it's equal to txType.STAKE

`TxType` is a TypeScript enum with the following shape:

    export enum TxType {
      STAKE = "stake",
      UNSTAKE = "unstake",
    }
    

**Check if a user can stake or not:**

`canStake` checks that the user has toggled the “stake” option, that `needsApproval` is false, and that a valid number has been inputted into the staking form.

**Loading transaction variable:**

![If txIsLoading is true, the screen above might render.](https://storage.googleapis.com/papyrus_images/2e936970f80f298f71bb449717b729bbcb758ae1a192d87e271fefd6f16371f8.png)

If txIsLoading is true, the screen above might render.

    const txIsLoading =
      approvalAwaitingSignature ||
      stakeAwaitingSignature ||
      unstakeAwaitingSignature ||
      txStatus === "loading";
    

This variable is a boolean that changes depending on the current transaction status. This value gets passed into the `Modal` component as a prop called `isClosable`. If `isClosable` is `true`, the close button in the modal will be disabled.

    <Modal
      className="w-full max-w-none xs:max-w-[360px] text-white"
      heading={modalLoadingStateHeadlines}
      isClosable={!txIsLoading}
      onCloseRequest={() => setShowStakingModal(false)}
    >
    

Above is a code snippet that shows the `Modal` component accepting `txIsLoading` as a prop for the `isClosable` variable

![](https://storage.googleapis.com/papyrus_images/adc10d6f9df1d338c0b64028a7cc55dcea6f621d26b98e4a0c1b7adde7d08249.png)

We want to disable the close button when a transaction is processing because closing the modal will make a user confused about the status of an active blockchain transaction. It’s bad UX when this happens. Blockchain users usually want a status update on their transaction and will often look at their screen until they see a “success” (or “failed”) confirmation of a completed transaction.

**Transaction Tracker**
-----------------------

![The Transaction Tracker takes many forms. This is the tracker after a successful staking transaction.](https://storage.googleapis.com/papyrus_images/52d66c59a3425defd7761fa91a5c1ce78831f098bbf433ef8b9a6a63ab0dcbfd.png)

The Transaction Tracker takes many forms. This is the tracker after a successful staking transaction.

This component was designed to be as generic as possible and to display the current state of transaction data. It’s used for the staking feature, as well as the claims feature (which will not be covered in this post).

The tracker takes in several props, including a transaction hash. This is a hash that gets passed into the Wagmi hook `useWaitForTransaction`, which returns the status of a transaction. The status is either: idle, loading, successful, or failed. The values of these status variables are used to determine which text and images to display in the transaction tracker. This provides users with feedback on their current stage in the staking and unstaking process.

**Action Buttons**
------------------

The staking modal has 1 main button to handle user actions. These actions include: approve, stake, unstaking. There’s also a toggle where users can switch between staking and unstaking.

`actionButtonsObject` is a function that takes in 4 arguments and returns an object. The return object takes the shape of `ActionButton`, which returns button labels and callback functions. 3 of these arguments are functions that were returned from Wagmi hooks.

    type ActionButton = {
      afterSuccess: { label: string; callback: () => void };
      afterFailure: { label: string; callback: () => void };
    }; 
    

**Here are some examples of how the button would change:**

*   After successfully calling the `“approve”` function, the button label will read “Continue”, and clicking on it would call the function `resetApproveAst`, which resets the status of the function. At this point, the user is permitted to stake tokens.
    
*   After a failed attempt at calling the `"approve"` function, the button label will read “Try again”, and clicking on it would also call `resetApproveAst`. The status of the approval function will reset, but the user will still be required to approve token spending before staking is allowed.
    

![After successfully approving, the button says “CONTINUE”. Clicking that button would call the resetApproveAst function.](https://storage.googleapis.com/papyrus_images/52d66c59a3425defd7761fa91a5c1ce78831f098bbf433ef8b9a6a63ab0dcbfd.png)

After successfully approving, the button says “CONTINUE”. Clicking that button would call the resetApproveAst function.

![After clicking the “CONTINUE” button in the previous screenshot, the staking modal will appear as shown above. The approved amount (20 AST) will persist in the form, allowing for a seamless user experience when staking the tokens by clicking “STAKE”.](https://storage.googleapis.com/papyrus_images/3e3a6e0bd30e367e542113df32b8ff578b11c9141e12e9943378b160a957a070.png)

After clicking the “CONTINUE” button in the previous screenshot, the staking modal will appear as shown above. The approved amount (20 AST) will persist in the form, allowing for a seamless user experience when staking the tokens by clicking “STAKE”.

The staking modal component also has a function called `actionButtonLogic`. This checks the status of various staking actions and returns a value based on which staking action is `true`. The return value of `actionButtonLogic` gets passed as props into the `TransactionTracker` component. The value that gets passed into the Transaction Tracker is what the user will see on the button.

The following code snippet shows how the `TransactionTracker` component is called in the `StakingModal` component.

    <TransactionTracker
      actionButtons={actionButtonLogic()}
      successContent={
        <span>
          You successfully {verb}{" "}
          <span className="text-white">{stakingAmountFormatted} AST</span>
        </span>
      }
      failureContent={"Your transaction has failed"}
      signatureExplainer={
        isApproval
          ? "To stake AST you will first need to approve the token spend."
          : undefined
        }
        txHash={currentTransactionHash}
      />
    

`signatureExplainer` is a prop in TransactionTracker. It pops up when a user has initiated a transaction, but has not yet signed the transaction in his or her wallet (for example MetaMask). The explainer is used to tell the user the next steps in the Staking flow.

![This screen will appear when a user's MetaMask wallet is open, but the transaction hasn't yet been signed.](https://storage.googleapis.com/papyrus_images/adc10d6f9df1d338c0b64028a7cc55dcea6f621d26b98e4a0c1b7adde7d08249.png)

This screen will appear when a user's MetaMask wallet is open, but the transaction hasn't yet been signed.

**Summary**
-----------

Here’s a video demo of the staking flow in action:

[https://vimeo.com/874258804](https://vimeo.com/874258804)

This article covers many of the functions that make up the staking modal, but there are several details I skipped over. Dive into the code on GitHub to see the rest of it. Here’s a high-level recap of how the staking modal works:

*   First, a user must approve the smart contract to spend AST tokens. In the StakingModal component, the boolean variable `needsApproval` checks whether or not a user can approve.
    
*   If `needsApproval` is true, the custom hook `useApproveAst` is enabled. Now, when a user clicks the “Approve” button in the TransactionTracker component, `useApproveAst` returns a callback function that writes the "approve" transaction to the blockchain.
    
*   After successfully approving, `needsApproval` should be set to false. Now the boolean value `canStake` determines whether a user can stake tokens or not.
    
*   If `canStake` is true, the custom hook `useStakeAst` will return the Wagmi callback function `stakeAst` (the original function name returned from Wagmi is `writeAsync`, but I renamed it in the staking Modal component). This process is similar to the token spending approval process, and unstaking tokens is similar to the token staking process.
    
*   `useApproveAst`, `useStakeAst`, `useUnstakeSast` all return objects called data, which contain a transaction hash (or undefined). These hooks also return the status of transactions: `"idle"`, `"loading"`, `"success"`, or `"failed"`.
    
*   These status variables and transaction hashes are passed into the TransactionTracker component, and give users feedback on their transactions.
    

The AirSwap Member Dashboard has more features than Staking, but I want to keep this blog post specific and won’t go into the other features today.

![Users can also vote on proposals that affect the AirSwap protocol.](https://storage.googleapis.com/papyrus_images/8f657ec1e83107296ae194bdc0284e67f9ab05454c7022622f8938ce92413766.png)

Users can also vote on proposals that affect the AirSwap protocol.

Conclusion
----------

Developing this Staking feature was one of my most challenging projects. I did countless refactors and put in many hours of work to make sure it came out as expected. I was fortunate to work alongside a more senior developer on this app. The guidance and mentorship were priceless.

Here’s an incomplete list of some of my learning lessons from building this feature:

*   It’s almost impossible to refactor code too much. Continuously refactoring code can seem tedious, but in the end, it makes you a more skilled developer.
    
*   Often the cause of bugs is having too much code, rather than too little. If there are bugs that are hard to fix, it can help to remove code until there aren’t any bugs. Once you reach the point of having no bugs, add in code until you can isolate exactly what was causing the original bugs.
    
*   Working with a mentor (or mentee) is priceless. Even if you’re building stuff that works, it’s nice to get other perspectives and hear of ways to improve your code.
    
*   Conducting code reviews for others can enhance your learning by providing insights into their coding structure.
    

I take pride in the work I've accomplished on this app, and I hope this blog post effectively conveys my enthusiasm during its development. Feel free to explore the app and test the staking feature. If you're interested, drop me a message for some Goerli AST to try it out. While there are areas for optimization, I am open to any feedback you may have

If you’ve read this far, I’d like to thank you for your attention.

**GitHub**: [https://github.com/airswap/airswap-voter-rewards](https://github.com/airswap/airswap-voter-rewards).

**Demo**: [http://dao.airswap.eth.limo](http://dao.airswap.eth.limo/).

---

*Originally published on [starrdev](https://paragraph.com/@starrdev/creating-a-staking-feature-with-react-and-typescript)*
