Tag Archives: react

React Components – From OOP To FP

In recent years, there has been an increasingly noticeable trend in software development that prompted developers to transition from object oriented programming (OOP) to functional programming (FP), whenever applicable. React appears to have joined the “movement” and its object oriented class-based React components have been transitioned to the functional components with React Hooks. A similar trend in the Scala world is the ad-hoc polymorphism with type classes as opposed to classic subtype polymorphism.

Given React‘s massive adoption rate and its API being evolved at a breakneck pace, we’re seeing a lot of React applications using a mix of both styles. Inevitably, some old method calls gradually get deprecated in order to align with the design principles of the new Hooks APIs. To maintain backward compatibility while evolving with the new functional approach, wrappers are being used to bundle the React Hooks with the class-based React components.

React Hooks in class-based components

For instance, for a User component to be able to reference the user-id from a HTTP GET query parameter via a React service, the useParams hook can be employed by being wrapped with the User component as a WrappedComponent.

import { connect } from "react-redux";
import { useParams } from "react-router-dom";
import UserDataService from "../services/user.service";

export const withParams = (WrappedComponent) => (props) => {
  const params = useParams();
  return <WrappedComponent {...props} params={params} />;
};
...

class User extends Component {
  ...
  componentDidMount() {
    this.getUser(this.props.params.id);
  }
  ...
  getUser(id) {
    UserDataService.find(id)
      .then(...)
      .catch(...);
  }
  ...
  render() {
    ...
    return {
      // Display UI
      ...
    }
  }
}

export default connect()(withParams(User));

A couple of notes:

  1. While a React hook looks like an ordinary function, it can only be used at the top level (or inside a custom React hook) and cannot be used within the React component class.
  2. If additional React Hooks are needed, they’d have to be successively wrapped. Say, if hooks useNavigate and useLocation are included, one would need to compose the withNavigate and withLocation top-level functions followed by wrapping them as WrappedComponent like below:
export default connect()(withLocation(withNavigate(withParams(User))));

React Hooks in functional components

Even though React Hooks are by design for functional components, the limitation of being at the top level (or from within custom hooks) still holds. That’s because the state of the hooks are internally “intertwined” with the React renderer throughout the rendering cycle of the React DOM.

Here’s what a functional equivalence of the above class-based component might look like:

import React, { useEffect } from "react";
import { useParams } from "react-router-dom";
import UserDataService from "../services/user.service";
...

const User = () => {
  const params = useParams();
  ...
  useEffect(() => {
    getUser(params.id);
  }, [params.id]);
  const getUser = (id) => {
    UserDataService.find(id)
      .then(...)
      .catch(...);
  }
  ...
  return (
    // Display UI
    ...
  )
}

export default User;

Comparing a class-based component with a functional component

As almost always the case, the easiest way to see the key differences between class-based and functional React components is to skim through the class-based and functional versions of a typical component with equivalent functionality.

First, a sample snippet of a class-based React component:

import React, { Component } from "react";
import { connect } from "react-redux";
import { Link, useParams } from "react-router-dom";
import { updateUser } from "../actions/user";
import UserDataService from "../services/user.service";

export const withParams = (WrappedComponent) => (props) => {
  const params = useParams();
  return <WrappedComponent {...props} params={params} />;
};

class User extends Component {  // User is a class

  constructor(props) {
    super(props);
    this.onChangeUsername = this.onChangeUsername.bind(this);
    this.onChangeEmail = this.onChangeEmail.bind(this);
    this.onChangeFirstName = this.onChangeFirstName.bind(this);
    this.onChangeLastName = this.onChangeLastName.bind(this);
    this.getUser = this.getUser.bind(this);
    this.updateContent = this.updateContent.bind(this);
    this.state = {
      currentUser: {
        id: null,
        username: "",
        email: "",
        firstName: "",
        lastName: ""
      },
      message: ""
    };
  }

  componentDidMount() {  // Component lifecycle function called after render() is called
    this.getUser(this.props.params.id);
  }

  // onChange functions for each input fields as class methods

  onChangeUsername(e) {
    const username = e.target.value;
    this.setState(function (prevState) {
      return {
        currentUser: {
          ...prevState.currentUser,
          setState: setState,
        },
      };
    });
  }

  onChangeEmail(e) {
    const email = e.target.value;
    this.setState(function (prevState) {
      return {
        currentUser: {
          ...prevState.currentUser,
          email: email,
        },
      };
    });
  }

  onChangeFirstName(e) {
    const firstName = e.target.value;
    this.setState((prevState) => ({
      currentUser: {
        ...prevState.currentUser,
        firstName: firstName,
      },
    }));
  }

  onChangeLastName(e) {
    const lastName = e.target.value;
    this.setState((prevState) => ({
      currentUser: {
        ...prevState.currentUser,
        lastName: lastName,
      },
    }));
  }

  getUser(id) {
    UserDataService.find(id)
      .then(...)    // Logic for handling return from
      .catch(...);  // service call skipped.
  }

  updateContent() {
    this.props
      .updateUser(this.state.currentUser.id, this.state.currentUser)
      .then(...)    // Logic for handling return from
      .catch(...);  // service call skipped.
  }

  render() {  // Rendering after constructor() is called
    const { currentUser, message } = this.state;
    const { loginUser } = this.props;
    return (
      <div className="list row">
        <div className="col-md-9">
          {currentUser && (currentUser.id === loginUser.id || loginUser.roles.includes('ROLE_ADMIN')) ? (
            <div className="edit-form">
              <h4>User</h4>
              <form>
                <div className="row mt-2">
                  <label className="col-md-3" htmlFor="id">ID</label>
                  <input
                    type="number"
                    className="col-md-9"
                    id="id"
                    value={currentUser.id}
                    disabled readOnly
                  />
                </div>
                <div className="row mt-2">
                  <label className="col-md-3" htmlFor="username">Username</label>
                  <input
                    type="text"
                    className="col-md-9"
                    id="username"
                    value={currentUser.username}
                    disabled readOnly
                  />
                </div>
                <div className="row mt-2">
                  <label className="col-md-3" htmlFor="email">Email</label>
                  <input
                    type="text"
                    className="col-md-9"
                    id="email"
                    value={currentUser.email}
                    onChange={this.onChangeEmail}
                  />
                </div>
                <div className="row mt-2">
                  <label className="col-md-3" htmlFor="firstName">First Name</label>
                  <input
                    type="text"
                    className="col-md-9"
                    id="firstName"
                    value={currentUser.firstName}
                    onChange={this.onChangeFirstName}
                  />
                </div>
                <div className="row mt-2">
                  <label className="col-md-3" htmlFor="lastName">Last Name</label>
                  <input
                    type="text"
                    className="col-md-9"
                    id="lastName"
                    value={currentUser.lastName}
                    onChange={this.onChangeLastName}
                  />
                </div>
              </form>
              <p className="text-warning">{message}</p>
              <button
                type="submit"
                className="btn btn-warning mt-2 mb-2"
                onClick={this.updateContent}
              >
                Update
              </button>
            </div>
          ) : (
            <div>
              <br />
              <p>User access not permitted!</p>
            </div>
          )}
        </div>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    loginUser: state.auth.user
  };
};

export default connect(mapStateToProps, { updateUser })(withParams(User));
// Using connect() for state info in Redux store

Next, an example of a functional style React component with equivalent functionality:

import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useParams } from "react-router-dom";
import { updateUser } from "../actions/user";
import UserDataService from "../services/user.service";

const User = () => {
  const params = useParams();
  const dispatch = useDispatch();
  const loginUser = useSelector(state => state.auth.user);

  const initUserState = {
    id: params.id,
    username: "",
    email: "",
    firstName: "",
    lastName: ""
  };

  const [currentUser, setCurrentUser] = useState(initUserState);
  const [message, setMessage] = useState("");

  const handleUserChange = event => {
    const { id, value } = event.target;
    setCurrentUser({ ...currentUser, [id]: value });
  };

  useEffect(() => {
    getUser(params.id);
  }, [params.id]);

  const getUser = (id) => {
    UserDataService.find(id)
      .then(...)    // Logic for handling return from
      .catch(...);  // service call skipped.
  };

  const updateContent = () => {
    dispatch(updateUser(currentUser.id, currentUser))
      .then(...)    // Logic for handling return from
      .catch(...);  // service call skipped.
  };

  return (
    <div className="list row">
      <div className="col-md-9">
        {currentUser && (currentUser.id === loginUser.id || loginUser.roles.includes('ROLE_ADMIN')) ? (
          <div className="edit-form">
            <h4>User</h4>
            <form>
              <div className="row mt-2">
                <label className="col-md-3" htmlFor="id">ID</label>
                <input
                  type="number"
                  className="col-md-9"
                  id="id"
                  value={currentUser.id}
                  disabled readOnly
                />
              </div>
              <div className="row mt-2">
                <label className="col-md-3" htmlFor="username">Username</label>
                <input
                  type="text"
                  className="col-md-9"
                  id="username"
                  value={currentUser.username}
                  disabled readOnly
                />
              </div>
              <div className="row mt-2">
                <label className="col-md-3" htmlFor="email">Email</label>
                <input
                  type="text"
                  className="col-md-9"
                  id="email"
                  value={currentUser.email}
                  onChange={handleUserChange}
                />
              </div>
              <div className="row mt-2">
                <label className="col-md-3" htmlFor="firstName">First Name</label>
                <input
                  type="text"
                  className="col-md-9"
                  id="firstName"
                  value={currentUser.firstName}
                  onChange={handleUserChange}
                />
              </div>
              <div className="row mt-2">
                <label className="col-md-3" htmlFor="lastName">Last Name</label>
                <input
                  type="text"
                  className="col-md-9"
                  id="lastName"
                  value={currentUser.lastName}
                  onChange={handleUserChange}
                />
              </div>
            </form>
            <p className="text-warning">{message}</p>
            <button
              type="submit"
              className="btn btn-warning mt-2 mb-2"
              onClick={updateContent}
            >
              Update
            </button>
          </div>
        ) : (
          <div>
            <br />
            <p>User access not permitted!</p>
          </div>
        )}
      </div>
    </div>
  );
};

export default User;

Differences between class-based and functional React components

Summarizing the key differences:

  1. Using of React Hooks – As highlighted earlier in this post, for the class-based component, the React hook needs to be wrapped as a WrappedComponent class, whereas it’s being used as a top-level function in a functional component.
  2. Class methods with binding vs regular functions – In the class-based component, each class method requires binding (i.e. .bind(this)) within the constructor so as to make keyword this refer to the component context, allowing the method to access component attributes such as this.props, this.state. On the other hand, functions for UI events in the functional component are no different from regular functions.
  3. State changes – State changes within a class-based component are typically initialized and maintained as class fields each with its corresponding event handler as a class method. In a functional component, state changes are managed via the useState React hook.
  4. Event handling – For the class-based component, in addition to binding, each input field needs to have its own onChange event handler (e.g. class method onChangeUsername), whereas for the functional component, it could be much less verbose by generalizing individual event handers into a single handleUserChange function as long as the onChange event handling logics for the input fields are similar.
  5. Rendering lifecycle – A class-based component has a lifecycle of constructor → render() → componentDidMount(), in that order. For a functional component, various kinds of React hooks have their specific “hooks” associated with the component’s lifecycle, whereas ordinary function calls within the main component are handled in programmatic order
  6. Managing state in React Redux store – Class-based components rely on the connect API along with specialty functions such as mapStateToProps to be called upon state changes in the Redux store to extract state info into the component. As for functional components, certain React Hooks including useSelector can be used for fetching state data from Redux store, as shown in the previous blog post.

Final thoughts

Based on what we’ve gone over, the functional approach to creating a React component obviously offers some advantages over the class-based approach. Code readability due to the minimizing of boilerplate code and consistency of treating event handlers as functions is perhaps the biggest plus.

Looking at the sample source code, the difference between class-based and functional components might seem drastic. But once we’ve had some fundamental understanding of how a React component handles UI events and maintains states throughout its lifecycle, the difference would become self-explanatory.

Nonetheless, migrating a large amount of class-based React components would still require significant investment in coding as well as testing. Since there is no sign support of class-based components will end any time soon, I’d say leaving the already proven working code as a mid-to-low priority tech debt is reasonable.

React Redux – Actions & Reducers

Having immersed in coding JavaScript exclusively using Node.js and React over the past couple of months, I’ve come to appreciate the versatility and robustness the “combo” has to offer. I’ve always liked the minimalist design of Node.js, and would always consider it a top candidate whenever building an app/API server is needed. Besides ordinary app servers, Node has also been picked on a few occasions to serve as servers for decentralized applications (dApps) that involve smart contract deployments to public blockchains. In fact, Node and React are also a popular tech stack for dApp frameworks such as Scaffold-ETH.

React & React Redux

React is relatively new to me, though it’s rather easy to pick up the basics from React‘s official site. And many tutorials out there showcase how to build applications using React along with the feature-rich toolset within the React ecosystem. For instance, this tutorial code repo offers helpful insight for developing a React application with basic CRUD.

React can be complemented with Redux that allows a central store for state update in the UI components. Contrary to the local state maintained within an React component (oftentimes used for handling interactive state changes to input form elements), the central store can be shared across multiple components for state update. That’s a key feature useful for the R&D project at hand.

Rather than just providing a plain global state repository for direct access, the React store is by design “decoupled” from the components. React Redux allows custom programmatic actions to be structured by user-defined action types. To dispatch an action, a component would invoke a dispatch() function which is the only mechanism that triggers a state change.

React actions & reducers

In general, a React action which is oftentimes dispatched in response to an UI event (e.g. a click on a button) mainly does two things:

  1. It carries out the defined action which is oftentimes an asynchronous function that invokes a user-defined React service which, for instance, might be a client HTTP call to a Node.js server.
  2. It connects with the Redux store and gets funneled into a reduction process. The reduction is performed thru a user-defined reducer which is typically a state aggregation of the corresponding action type.

An action might look something like below:

const myAction = () => async (dispatch) => {
  try {
    const res = await myService.someFunction();
    dispatch({
      type: someActionType,
      payload: res.data,
    });
  } catch (err) {
    ...
  }
};

whereas a reducer generally has the following function signature:

const myReducer = (currState = prevState, action) => {
  const { type, payload } = action;
  switch (type) {
    case someActionType:
      return someFormOfPayload;
    case anotherActionType:
      return anotherFormOfPayload;
    ...
    default:
      return currState;
  }
};

Example of a React action

${react-project-root}/src/actions/user.js

import {
  CREATE_USER,
  RETRIEVE_USERS,
  UPDATE_USER,
  DELETE_USER
} from "./types";
import UserDataService from "../services/user.service";
export const createUser = (username, password, email, firstName, lastName) => async (dispatch) => {
  try {
    const res = await UserDataService.create({ username, password, email, firstName, lastName });
    dispatch({
      type: CREATE_USER,
      payload: res.data,
    });
    return Promise.resolve(res.data);
  } catch (err) {
    return Promise.reject(err);
  }
};
export const findUsersByEmail = (email) => async (dispatch) => {
  try {
    const res = await UserDataService.findByEmail(email);
    dispatch({
      type: RETRIEVE_USERS,
      payload: res.data,
    });
  } catch (err) {
    console.error(err);
  }
};
export const updateUser = (id, data) => async (dispatch) => {
  try {
    const res = await UserDataService.update(id, data);
    dispatch({
      type: UPDATE_USER,
      payload: data,
    });
    return Promise.resolve(res.data);
  } catch (err) {
    return Promise.reject(err);
  }
};
export const deleteUser = (id) => async (dispatch) => {
  try {
    await UserDataService.delete(id);
    dispatch({
      type: DELETE_USER,
      payload: { id },
    });
  } catch (err) {
    console.error(err);
  }
};

Example of a React reducer

${react-project-root}/src/reducers/users.js

import {
  CREATE_USER,
  RETRIEVE_USERS,
  UPDATE_USER
  DELETE_USER,
} from "../actions/types";
const initState = [];
function userReducer(users = initState, action) {
  const { type, payload } = action;
  switch (type) {
    case CREATE_USER:
      return [...users, payload];
    case RETRIEVE_USERS:
      return payload;
    case UPDATE_USER:
      return users.map((user) => {
        if (user.id === payload.id) {
          return {
            ...user,
            ...payload,
          };
        } else {
          return user;
        }
      });
    case DELETE_USER:
      return users.filter(({ id }) => id !== payload.id);
    default:
      return users;
  }
};
export default userReducer;

React components

Using React Hooks which are built-in functions, the UI-centric React components harness powerful features related to handling states, programmatic properties, parametric attributes, and more.

To dispatch an action, the useDispatch hook for React Redux can be used that might look like below:

import { useDispatch, useSelector } from "react-redux";
...
  const dispatch = useDispatch();
  ...
    dispatch(myAction(someRecord.id, someRecord))  // Corresponding service returns a promise
      .then((response) => {
        setMessage("myAction successful!");
        ...
      })
      .catch(err => {
        ...
      });
  ...

And to retrieve the state of a certain item from the Redux store, the userSelector hook allow one to use a selector function to extract the target item as follows:

  const myRecords = useSelector(state => state.myRecords);  // Reducer myRecords.js

Example of a React component

${react-project-root}/src/components/UserList.js

import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { retrieveUsers, findUsersByEmail } from "../actions/user";
const UserList = () => {
  const dispatch = useDispatch();
  const users = useSelector(state => state.users);
  const [currentUser, setCurrentUser] = useState(null);
  const [currentIndex, setCurrentIndex] = useState(-1);
  const [searchEmail, setSearchEmail] = useState("");
  useEffect(() => {
    dispatch(retrieveUsers());
  }, [dispatch]);
  const onChangeSearchEmail = e => {
    const searchEmail = e.target.value;
    setSearchEmail(searchEmail);
  };
  const refreshData = () => {
    setCurrentUser(null);
    setCurrentIndex(-1);
  };
  const setActiveUser = (user, index) => {
    setCurrentUser(user);
    setCurrentIndex(index);
  };
  const findByEmail = () => {
    refreshData();
    dispatch(findUsersByEmail(searchEmail));
  };
  return (
    <div className="list row">
      <div className="col-md-9">
        <div className="input-group mb-3">
          <input
            type="text"
            className="form-control"
            id="searchByEmail"
            placeholder="Search by email"
            value={searchEmail}
            onChange={onChangeSearchEmail}
          />
          <div className="input-group-append">
            <button
              className="btn btn-warning m-2"
              type="button"
              onClick={findByEmail}
            >
              Search
            </button>
          </div>
        </div>
      </div>
      <div className="col-md-5">
        <h4>User List</h4>
        <ul className="list-group">
          {users &&
            users.map((user, index) => (
              <li
                className={
                  "list-group-item " + (index === currentIndex ? "active" : "")
                }
                onClick={() => setActiveUser(user, index)}
                key={index}
              >
                <div className="row">
                  <div className="col-md-2">{user.id}</div>
                  <div className="col-md-10">{user.email}</div>
                </div>
              </li>
            ))}
        </ul>
        <Link to="/add-user"
          className="btn btn-warning mt-2 mb-2"
        >
          Create a user
        </Link>
      </div>
      <div className="col-md-7">
        {currentUser ? (
          <div>
            <h4>User</h4>
            <div className="row">
              <div className="col-md-3 fw-bold">ID:</div>
              <div className="col-md-9">{currentUser.id}</div>
            </div>
            <div className="row">
              <div className="col-md-3 fw-bold">Username:</div>
              <div className="col-md-9">{currentUser.username}</div>
            </div>
            <div className="row">
              <div className="col-md-3 fw-bold">Email:</div>
              <div className="col-md-9">{currentUser.email}</div>
            </div>
            <div className="row">
              <div className="col-md-3 fw-bold">First Name:</div>
              <div className="col-md-9">{currentUser.firstName}</div>
            </div>
            <div className="row">
              <div className="col-md-3 fw-bold">Last Name:</div>
              <div className="col-md-9">{currentUser.lastName}</div>
            </div>
            <Link
              to={"/user/" + currentUser.id}
              className="btn btn-warning mt-2 mb-2"
            >
              Edit
            </Link>
          </div>
        ) : (
          <div>
            <br />
            <p>Please click on a user for details ...</p>
          </div>
        )}
      </div>
    </div>
  );
};
export default UserList;

It should be noted that, despite having been stripped down for simplicity, the above sample code might still have included a little bit too much details for React beginners. For now, the primary goal is to highlight how an action is powered by function dispatch() in accordance with a certain UI event to interactively update state in the Redux central store thru a corresponding reducer function.

In the next blog post, we’ll dive a little deeper into React components and how they have evolved from the class-based OOP (object oriented programming) to the FP (functional programming) style with React Hooks.

Ethereum-compatible NFT On Avalanche

While blockchain has been steadily gaining increasing attention from the general public over the past couple of years, it’s NFT, short for non-fungible token, that has recently taken the center stage. In particular, NFT shines in the area of provenance of authenticity. By programmatically binding a given asset to a unique digital token referencing immutable associated transactions on a blockchain, the NFT essentially serves as the “digital receipt” of the asset.

Currently Ethereum is undergoing a major upgrade to cope with future growth of the blockchain platform which has been suffering from low transaction rate and high gas fee due to the existing unscalable Proof of Work consensus algorithm. As described in a previous blockchain overview blog post, off-chain solutions including bridging the Ethereum main chain with layer-2 subchains such as Polygon help circumvent the performance issue.

Avalanche

Some layer-1 blockchains support Ethereum’s NFT standards (e.g. ERC-721, ERC-1155) in addition to providing their own native NFT specs. Among them is Avalanche which has been steadily growing its market share (in terms of TVL), trailing behind only a couple of prominent layer-1 blockchains such as Solana and Cardano.

With separation of concerns (SoC) being one of the underlying design principles, Avalanche uses a subnet model in which validators on the subnet only operate on the specific blockchains of their interest Also in line with the SoC design principle, Avalanche comes with 3 built-in blockchains each of which serves specific purposes with its own set of API:

  • Exchange Chain (X-Chain) – for creation & exchange of digital smart assets (including its native token AVAX) which are bound to programmatic governance rules
  • Platform Chain (P-Chain) – for creating & tracking subnets, each comprising a dynamic group of stake holders responsible for consensually validating blockchains of interest
  • Contract Chain (C-Chain) – for developing smart contract applications

NFT on Avalanche

Avalanche allows creation of native NFTs as a kind of its smart digital assets. Its website provides tutorials for creating such NFTs using its Go-based AvalancheGo API. But perhaps its support of the Ethereum-compatible NFT standards with much higher transaction rate and lower cost than the existing Ethereum mainnet is what helps popularize the platform.

In this blog post, we’re going to create on the Avalanche platform ERC-721 compliant NFTs which require programmatic implementation of their sale/transfer terms in smart contracts. C-Chain is therefore the targeted blockchain. And rather than deploying our NFTs on the Avalanche mainnet, we’ll use the Avalanche Fuji Testnet which allows developers to pay for transactions in test-only AVAX tokens freely available from some designated crypto faucet.

Scaffold-ETH: an Ethereum development stack

A code repository of comprehensive Ethereum-based blockchain computing functions, Scaffold-ETH, offers a suite of tech stacks best for fast prototyping development along with sample code for various use cases of decentralized applications. The stacks include Solidity, Hardhat, Ether.js and ReactJS.

The following softwares are required for installing Scaffold-ETH, building and deploying NFT smart contracts:

Launching NFTs on Avalanche using a customized Scaffold-ETH

For the impatient, the revised code repo is at this GitHub link. Key changes made to the original branch in Scaffold-ETH will be highlighted at the bottom of this post.

To get a copy of Scaffold-ETH repurposed for NFTs on Avalanche, first git-clone the repo:

git clone https://github.com/oel/avalanche-scaffold-eth-nft avax-scaffold-eth-nft

Next, open up a couple of shell command terminals and navigate to the project-root (e.g. avax-scaffold-eth-nft).

Step 1: From the 1st shell terminal, install the necessary dependent modules.

cd avax-scaffold-eth-nft/
yarn install

Step 2: From the 2nd terminal, specify an account as the deployer.

Choose an account that owns some AVAX tokens (otherwise, get free tokens from an AVAX faucet) on the Avalanche Fuji testnet and create file packages/hardhat/mnemonic.txt with the account’s 12-word mnemonic in it.

cd avax-scaffold-eth-nft/
yarn account
yarn deploy --network fujiAvalanche

For future references, the “deployed at” smart contract address should be saved. Transactions oriented around the smart contract can be reviewed at snowtrace.io.

Step 3: Back to the 1st terminal, start the Node.js server at port# 3000.

yarn start

This will spawn a web page on the default browser (which should have been installed with the MetaMask extension).

Step 4: From the web browser, connect to the MetaMask account which will receive the NFTs

Step 5: Back to the 2nd terminal, mint the NFTs.

yarn mint --network fujiAvalanche

The address of the NFT recipient account connected to the browser app will be prompted. Upon successful minting, images of the NFTs should be automatically displayed on the web page.

To transfer any of the NFTs to another account, enter the address of the account to be transferred to and click “transfer”. Note that the account connected to the browser app would need to own some AVAX tokens (again if not, get free tokens from an AVAX faucet).

The web page upon successful minting should look like below:

Avalanche NFTs using Scaffold-ETH (MetaMask connected)

Key changes made to the original Scaffold-ETH branch

It should be noted that Scaffold-ETH is a popular code repo under active development. The branch I had experimented with a few months ago is already markedly different from the same branch I git-cloned for custom modification. That prompted me to clone a separate repo to serve as a “snapshot” of the branch, rather than just showing my modifications to an evolving code base.

Below are the main changes made to the Scaffold-ETH Simple NFT Example branch git-cloned on March 30:

Hardhat configuration script: packages/hardhat/hardhat.config.js

The defaultNetwork value in the original Hardhat configuration script is “localhost” by default, assuming a local instance of a selected blockchain is in place. The following change sets the default network to the Fuji testnet, whose network configuration parameters need to be added as shown below.

const defaultNetwork = "fujiAvalanche";
# const defaultNetwork = "mainnetAvalanche";
...
module.exports = {
  ...
  networks: {
    ...
    fujiAvalanche: {
      url: "https://api.avax-test.network/ext/bc/C/rpc",
      gasPrice: 225000000000,
      chainId: 43113,
      accounts: {
        mnemonic: mnemonic(),
      },
    },
    ...

Note that with the explicit defaultNetwork value set to “fujiAvalanche”, one could skip the --network fujiAvalanche command line option in the smart contract deploy and mint commands.

ReactJS main app: packages/react-app/src/App.jsx

To avoid compilation error, the following imports need to be moved up above the variable declaration section in main Node.js app.

import { useContractConfig } from "./hooks"
import Portis from "@portis/web3";
import Fortmatic from "fortmatic";
import Authereum from "authereum";

...
const targetNetwork = NETWORKS.fujiAvalanche;
# const targetNetwork = NETWORKS.mainnetAvalanche

Minting script: packages/hardhat/scripts/mint.js

A few notes:

  • The square-shaped animal icon images for the NFTs used in the minting script are from public domain sources. Here’s the link to the author’s website.
  • Node module prompt-sync is being used (thus is also added to the main package.json dependency list). It’s to avoid having to hardcode the NFT recipient address in the minting script.
  • The code below makes variable toAddress a dynamic input value and replaces the original NFT images with the square-styling images along with a modularized mintItem function.
...
const prompt = require('prompt-sync')();

const delayMS = 5000  // Increase delay as needed!

const main = async () => {

  // ADDRESS TO MINT TO:
  // const toAddress = "0x36f90A958f94F77c26614DB170a5C8a7DF062A90"
  const toAddress = prompt("Enter the address to mint to: ");

  console.log("\n\n 🎫 Minting to "+toAddress+"...\n");

  const { deployer } = await getNamedAccounts();
  const yourCollectible = await ethers.getContract("YourCollectible", deployer);

  // Item #1

  const iconCrocodile = {
    "description": "Squared Croc Icon",
    "external_url": "https://blog.genuine.com/",
    "image": "https://blog.genuine.com/wp-content/uploads/2022/03/Crocodile-icon.png",
    "name": "Squared Crocodile",
    "attributes": [
       {
         "trait_type": "Color",
         "value": "Green"
       }
    ]
  }
  mintItem(iconCrocodile, yourCollectible, toAddress)

  await sleep(delayMS)

  // Item #2

  const iconDuck = {
    "description": "Squared Duck Icon",
    "external_url": "https://blog.genuine.com/",
    "image": "https://blog.genuine.com/wp-content/uploads/2022/03/Duck-icon.png",
    "name": "Squared Duck",
    "attributes": [
       {
         "trait_type": "Color",
         "value": "Yellow"
       }
    ]
  }
  mintItem(iconDuck, yourCollectible, toAddress)

  await sleep(delayMS)

  // Item #3

  const iconEagle = {
    "description": "Squared Eagle Icon",
    "external_url": "https://blog.genuine.com/",
    "image": "https://blog.genuine.com/wp-content/uploads/2022/03/Eagle-icon.png",
    "name": "Squared Eagle",
    "attributes": [
       {
         "trait_type": "Color",
         "value": "Dark Gray"
       }
    ]
  }
  mintItem(iconEagle, yourCollectible, toAddress)

  await sleep(delayMS)

  // Item #4

  const iconElephant = {
    "description": "Squared Elephant Icon",
    "external_url": "https://blog.genuine.com/",
    "image": "https://blog.genuine.com/wp-content/uploads/2022/03/Elephant-icon.png",
    "name": "Squared Elephant",
    "attributes": [
       {
         "trait_type": "Color",
         "value": "Light Gray"
       }
    ]
  }
  mintItem(iconElephant, yourCollectible, toAddress)

  await sleep(delayMS)

  // Item #5

  const iconFish = {
    "description": "Squared Fish Icon",
    "external_url": "https://blog.genuine.com/",
    "image": "https://blog.genuine.com/wp-content/uploads/2022/03/Fish-icon.png",
    "name": "Squared Fish",
    "attributes": [
       {
         "trait_type": "Color",
         "value": "Blue"
       }
    ]
  }
  mintItem(iconFish, yourCollectible, toAddress)

  await sleep(delayMS)

  console.log("Transferring Ownership of YourCollectible to "+toAddress+"...")

  await yourCollectible.transferOwnership(toAddress, { gasLimit: 8000000 });  // Increase limit as needed!

  await sleep(delayMS)

  ...
}

async function mintItem(item, contract, mintTo, limit = 8000000) {  // Increase limit as needed!
  console.log("Uploading `%s` ...", item.name)
  const uploaded = await ipfs.add(JSON.stringify(item))

  console.log("Minting `%s` with IPFS hash ("+uploaded.path+") ...", item.name)
  await contract.mintItem(mintTo,uploaded.path,{gasLimit:limit})
}
...