Try โ€‚โ€‰HackMD

dApp Scaffolding with React.



Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’



Getting Started ๐Ÿš€

  1. yarn or npm i
  2. Read this README file ๐Ÿ˜Ž

Contents

  1. ESLint and Prettier - ready
  2. ENV Setting
  3. Reusable Component (Preview)
  4. Custom Hooks (Preview)
  5. EthersProvider
  6. MessageNet
  7. Components Directory Structure
  8. Custom Hooks Properties
  9. Reusable Components Properties

ESLint and Prettier - ready (optional) ๐Ÿช


If you want to use ESLint and Prettier while developing just copy sample settings inside

sample-eslint.txt and sample-prettier.txt then create these two files:

touch .eslintrc .prettierrc

However, if you want to opt out in using these, just uninstall the Dev Dependencies:

yarn remove @babel/eslint-parser @babel/preset-react eslint eslint-config-airbnb eslint-config-prettier eslint-config-react-app eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks prettier pretty-quick
NOTE: PLEASE ALSO CHECK YOUR IDE SETTINGS. ๐Ÿ‘‹๐Ÿป

ENV ๐Ÿช


An Infura ID is needed for Wallet Connect.

REACT_APP_INFURA_ID=
REACT_APP_CONTRACT_ADDRESS=

Reusable Components (TL;DR) ๐Ÿ”ฅ


As of now, these are the reusable components in this scaffolding:

  • <Connector />
    • This is a button that has methods for connecting/disconnecting wallet.
  • <Crementor />
    • This is a button that will increment/decrement quantity to be minted.
  • <Message />
    • Used for notification banners
  • <NiceButton />
    • A button which you can add icon or text or events
  • <Collapsible />
    • Can be toggled on/off, mostly used for FAQ

Jump to bottom section to read more about these components.


Custom Hooks (TL;DR) ๐Ÿ”ฅ


As of now, these are the custom hooks available:

  • useSignature
    • Allows to easily sign a message using provider (e.g., Metamask)

Jump to bottom section to read more about these components.


EthersProvider ๐ŸŒด


The EthersProvider is the main component. It will return instances of 'ether' and 'web3Modal', and its own ethersProvider util data. Purpose is to just pull the instances instead of always importing it. These are all the data that EthersProvider can return:

{
    ethers,
    web3Modal,
    address,
    chainId,
    provider,
    signer,
    isConnected,
    isMetamaskInstalled,
    ethersProvider: {
        connect: () => {},
        disconnect: () => {},
        switchNetwork: () => {},
    },
}

Also, the EthersProvider has a askOnLoad props. If it is set to true (its default state), on site's load it will trigger the Metamask to prompt asking the user to switch to Ethereum network. Setting it to false will prevent it from prompting. Check sample usage below.

There are two (2) main scenarios to implement initialization.

SCENARIO A1: You want to manually handle everything:

const { ethers, web3Modal } = useContext(EthersContext);

// States
const [isConnected, setIsConnected] = useState(false);
const [userAccount, setUserAccount] = useState(null);
const [chainId, setChainId] = useState(null);
const [signer, setSigner] = useState(null);
const [provider, setProvider] = useState(null);

// Handlers
const onConnectHandler = async () => {
    try {
        const instance = await web3Modal.connect();
        const provider = new ethers.providers.Web3Provider(instance);
        const { chainId } = await provider.getNetwork();
        const signer = await provider.getSigner();
        const address = await signer.getAddress();

        setProvider(provider);
        setUserAccount(address);
        setChainId(chainId);
        setSigner(signer); // optional
        setIsConnected(true);
    } catch ({ message }) {
        if (!message) return;

        const errorMsg = message.toLowerCase();
        if (errorMsg.includes('user rejected')) {
            console.log('Wallet connection was cancelled!');
        }
    }
};

SCENARIO A2: Still scenario A, but this is a shorter version:

const { ethers, ethersProvider } = useContext(EthersContext);

// States
const [isConnected, setIsConnected] = useState(false);
const [userAccount, setUserAccount] = useState(null);
const [chainId, setChainId] = useState(null);
const [signer, setSigner] = useState(null);
const [provider, setProvider] = useState(null);

// Handlers
const onConnectHandler = async () => {
    try {
        const { provider, address, chainId, signer } = await ethersProvider.connect();
        if (provider) setProvider(provider);
        if (address) setUserAccount(address);
        if (chainId) setChainId(chainId);
        if (signer) setSigner(signer); // optional
        setIsConnected(true);
    } catch (e) {}
};

The ethersProvider util inside scenario A2 will handle all initialization and errors.


SCENARIO B: You have multiple components, and you want the Navigation component to handle the initialization and the rest of the other components can access the data, e.g., Body component can access all of it.

import { useContext } from 'react';

import { EthersContext } from '../store/all-context-interface';

const Component = () => {
  const { ethers, address, provider, chainId, signer, isConnected } = useContext(EthersContext);
  
  // ...rest of the code
}

What is happening is that the <Navigation /> component which contains the <Connector /> component handles all connection and disconnection. So your <Body /> component will become a receiver only and can still access all data through the EthersProvider context, based on the code above.

Scenario B - Sample Usage

function App() {
    return (
        <EthersProvider askOnLoad={false}>
            <Navigation />
            <Body />
            <FAQ />
        </EthersProvider>
    );
}


MessageNet ๐ŸŒด


The MessageNet is somehow also acts as a provider. Instead of importing the <Message /> reusable component and use it for notifications, errors, etc., we will just encapsulate all components that need to spit out some UI messages. See example below:

// HomePage
import { useContext } from 'react';

import { MsgNetContext } from '../store/all-context-interface';

const HomePage = () => {
    const { setMsg } = useContext(MsgNetContext);
    
    // Handlers
    const mintHandler = async () => {
        try {
            // ... mint logic
            setMsg('Nice, minted an NFT!', 'success');
        } catch (e) {
            setMsg('Something went wrong', 'warning');
        }
    }
    
    // ...rest of the code
}
// App
function App() {
    return (
          <MessageNet>
            <EthersProvider askOnLoad={false}>
              <HomePage />
            </EthersProvider>
          </MessageNet>
    );
}


Components Directory Structure ๐Ÿšง


Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

You can find these inside src/components directory. Though opinionated, I would highly suggest we do it like this. Just to stay a bit organized.

  1. core โ€“> Will contain all utility components, like the one that handles connections, processes data, etc.

  2. pages โ€“> Are simply for pages and layouts. If it's only a landing page, you can still call it Home. Then inside it you can put all layouts pertaining to <Home />.

  3. ui โ€“> Will contain all reusable UI components. Check next section.





Custom Hooks Properties ๐Ÿ”ฅ


useSignature

This custom hooks allows to quickly sign a message using a provider, for example, Metamask. You just pass a message you wanna sign and then gets all the data back. See example:

const { sigData, signMessage } = useSignature();

// Will sign the message
const runSig = (msg) => signMessage(msg);

// Returns all data: signature, address, message
const logSig = () => console.log(sigData);

// address: "0x76fdd1d979eA8dd2a7561C14f763A3CdFd9648e7"
// message: "test message here"
// signature: "0xf9a278786520200b28eb6ed909242d0b9e26631e7cc3..."

The signMessage function takes 2 arguments, message and signer. The message is required but the signer is optional since this boilerplate is already encapsulated inside EthersProvider component.



Reusable Components Properties ๐Ÿ”ฅ


<Connector />

This core component is responsible for connecting and disconnecting the user. Below are the props and default props for this component:

const Connector = ({
    forceSwitch = true,
    network = '0x1',
    connText = 'CONNECT',
    disconnText = 'DISCONNECT',
    bgColor = '#5e356c',
    hoverBgColor = '#2d996c',
    lineColor = '#b3bdb3',
    hoverLineColor = '#dae7da',
    lineSize = 1,
    curve = 23,
    textSpace = 2,
    textSize = 1.3,
    font = 'Helvetica',
    textColor = 'white',
    textWeight = 700,
    height = 12,
    width = 210,
}) => {...}

forceSwitch is useful when you want to trigger network change after the connecting process. Especially, if askOnLoad is set to false in <EthersProvider askOnLoad={false} />. Default network or chain is '0x1' which is the Ethereum network.


<NiceButton />

This is a customizable and reusable button component. This can work as a link, a button with an icon, or just a simple button. Below are the props and default props for this component:

const NiceButton = ({
    curve = 7,
    icon = null,
    iconColor = 'white',
    hoverIconColor = '#98ea9a',
    link = null,
    linkTarget = '_blank',
    text = 'Pop Block',
    lineColor = '#5e5e5e',
    hoverLineColor = '#716e6e',
    lineSize = 1,
    bgColor = '#a276bf3b',
    hoverBgColor = '#694A7D3B',
    textColor = 'white',
    textWeight = 600,
    textSize = 1.3,
    textSpace = 0,
    font = 'Helvetica',
    wordSpace = 3,
    width = 15,
    height = 48,
    style = null,
    method = null,
}) => {...}

Here is a sample usage of this component using it as a button with an icon:

import { FaFingerprint } from 'react-icons/fa';

const Component = () => {
    return (
        <div className="...">
            <NiceButton
                text="mint"
                method={onClickMintHandler}
                textSize={1.3}
                height={42}
                width={20}
                curve={13}
                bgColor="#0C0D0C"
                hoverBgColor="#0C0D0C"
                icon={<FaFingerprint size={18} />}
            />
        </div>
    )
}

Recommended icon package to use is: https://react-icons.github.io/react-icons/. If the link props is present it will automatically be converted to link-type button.


<Crementor />

This reusable component is used when increment and decrement functions are needed. Below are the props and default props of this component:

const Crementor = ({
    value = 0,
    width = 300,
    height = 10,
    curve = 50,
    bgColor = '#2b3b5e',
    lineColor = '#818181',
    hoverLineColor = '#ac9595',
    btnColor = '#dbdbdb',
    hoverBtnColor = '#98ea9a',
    onDecrement,
    onIncrement,
}) => {...}

And, here is the sample usage of this component:

const Component = () => {
    const onIncrementHandler = () => {
        setDefValue(defValue + 1);
    };

    const onDecrementHandler = () => {
        if (defValue === 0) return;
        setDefValue(defValue - 1);
    };
    
    return (
        <div className="...">
            <Crementor
                value={defValue}
                bgColor="#2b3b5e"
                lineColor="#818181"
                onIncrement={onIncrementHandler}
                onDecrement={onDecrementHandler}
            />
        </div>
    )
}



TODO - continueโ€ฆ