Write Your First DAPP in 10mins (3/3)

Write Your First DAPP in 10mins (3/3)

Learn how to use the most popular frontend library to make a DAPP user interface and this is the last part of the full stack DAPP article tutorial.

With courage you will dare to take risks, have the strength to be compassionate, and the wisdom to be humble. Courage is the foundation of integrity. -Mark Twain

Hi, it is me again. Thanks for stopping by.

In this article, I will show you how to integrate the smart contract we made in part 1 of this article series, Check out the article here, with React.

What is React?

From React's official documentation.

React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes.

If you already know how to use react, this article will be easy for you to understand, but if you don't I will recommend some resources below to get you started.

I'll advise you to pause and read the official documentation to understand concepts like rendering elements, components and props, state and lifecycle, handling events, and conditional rendering.

React Learning Resources

We will be using react-bootstrap for our UI, react-identicons for pictorial representation of addresses, and web3.js for interacting with the smart contract on our truffle blockchain network.

Let's get started.

In your project path,

 c:\path\to\project> npx create-react-app client

A scaffolded react application will be in the client directory.

c:\path\to\project>cd client
c:\path\to\project\client>yarn add web3 react-bootstrap react-identicons

While installing these packages, let's make some changes to our truffle configuration file.

We can specify the directory where our compiled and migrated contracts will be. Our contracts abi files will be in ~/client/src/contracts.

In your truffle-config.js change it to this

image.png

Now our abi files will live in ~/client/src/contracts. Start another command line and execute the following commands. These commands migrate the smart contract to the new directory. Delete the former abi folder.

//step 1
c:\path\to\project>truffle develop 

//step 2
truffle(develop)> compile

//step 3
truffle(develop)> migrate

//step 4: let's add our initial post to avoid errors from PostCount. Skip this step 
//and see for yourself.
truffle(develop)> c = await EMeet.deployed()
undefinded
truffle(develop)> c.createPost("Initial post")
truffle(develop)> c.commentOnPost(1,"Initial comment")

It is easy to access contracts from the client-side folder now. However, we need to set up a wallet that enables us to communicate with the blockchain application.

Setting up MetaMask

MetaMask is a popular crypto wallet and getaway to blockchain applications. Head over to their website to download the browser extension. Setting it up is pretty straightforward.

Next, we need to connect our MetaMask with our truffle blockchain (develop), this guide explains in detail how to connect truffle to MetaMask.

Setting up React UIs

This section will be more of boilerplates to help you set up the UI and then make changes accordingly.

After installing those packages to the react app, run yarn start to start the project.

If everything is done right, you should see a react page.

Creating more Components and Laying Out Our Application

In ~/client/src folder, create a new folder named components. Create the following files nav.js, comment-card.js, post-card.js, post-container.js, and profile-container.js.

In your App.js

//in ~/App.js
import 'bootstrap/dist/css/bootstrap.min.css';
import {Container} from 'react-bootstrap'
import Nav from './components/nav'
import PostContainer from './components/post-container'
import ProfileContainer from './components/profile-container'


function App() {
  return (
    <div style={{position:"relative"}}>
      <Nav />
      <Container className="row m-auto mb-5" style={{zIndex:5}}>
        <ProfileContainer />
        <PostContainer />
      </Container>
    </div>
  );
}

export default App;
Create Components
//in ~/components/post-container.js

import {Col, Form, Button} from 'react-bootstrap';
import PostCard from './post-card'

const PostContainer = function(){
  return <Col lg="5" md="8" className="m-auto mt-4 mt-md-5 shadow-sm">
        <Form className="p-1">
          <Form.Group className="mb-3">
          <Form.Control as="textarea" rows={3}  style={{resize:"none"}} placeholder="What's on your mind?" />
          </Form.Group>
          <Button variant="primary scale-down">Publish</Button>
        </Form>
        <div style={{maxHeight:"60vh",overflowY:"auto"}} className="mt-2 no-scrollbar">
        <PostCard  />
        <PostCard  />
        <PostCard  />
        <PostCard  />
        <PostCard  />
        <PostCard  />
        </div>
        </Col>
}

export default PostContainer;
//in ~/components/profile-card.js

import {Col, Card} from 'react-bootstrap';

const ProfileContainer = function(){
  return (<Col lg="3" md="1" className="mt-4 me-2 d-none d-lg-block">
        <Card style={{ width: '18rem' }}>
        <Card.Img
          variant="top"
          src="https://picsum.photos/200/180"
          width={200}
          height={200}
          />
          <Card.Body>
          <Card.Title>Your Details</Card.Title>
          <Card.Text>
          Address: 7793i3ij20e0
          </Card.Text>
          </Card.Body>
        </Card>
   </Col>)
}

export default ProfileContainer;
//in ~/components/post-card.js

import {Image,Form, Container, Button, Accordion, useAccordionButton} from 'react-bootstrap';
import CommentCard from './comment-card';

const PostCommentToggler = ({children, eventKey, callback}) => {
  const openOnClick = useAccordionButton(
    eventKey,
    () => callback && callback(eventKey),
  );
  return (<span className="ms-2 strong" onClick={openOnClick}> {children}</span>)
}



const PostCard = function(){
  const tipPost = () => {
    console.log("ether");
  }
  return(
    <Accordion className="my-2 border border-secondary p-2">

        <div className="d-flex justify-content-start ">
          <Image src="https://picsum.photos/200/180" width={40} height={40} roundedCircle />
          <small className="mt-1 mx-2 p-1  text-truncate">
            <strong>
              8yu4983yhin8h983h4i4h983h89y4h984
            </strong>
          </small>
        </div>
        <div className="post-content text-justify p-2 m-1 mb-2">
          Laboris labore dolor est veniam, iudicem summis commodo, admodum labore eiusmod,noster e ut sint deserunt, litteris illustriora sed occaecat, litteris quid culpa admodum fore, quid ullamco a domesticarum.
         </div>

       <div className="d-flex justify-content-between mt-2 w-100 px-2 border border-top-0 border-end-0 border-start-0 border-bottom-2 py-2 bg-light">
         <div>
          4 Ethers
          <span
          className="bg-primary badge text-light rounded py-1 px-2 ms-1 scale-down"
          onClick={tipPost}
          >
          Tip Post
          </span>
         </div>
         <div>
          4
            <PostCommentToggler  eventKey="0">Comments</PostCommentToggler>
         </div>
      </div>
      <Accordion.Collapse eventKey="0">
        <Container className="mt-4">
          <Form className="p-1">
            <Form.Group className="mb-3">
            <Form.Control as="textarea" rows={2}  style={{resize:"none"}} placeholder="This is interesting..." />
            </Form.Group>
            <Button variant="secondary text-white w-100 scale-down">Post Comment</Button>
          </Form>
          <div style={{maxHeight:"250px",overflowY:"auto"}} className="mt-2 no-scrollbar">
          <CommentCard />
          <CommentCard />
          <CommentCard />
          <CommentCard />
          <CommentCard />
          <CommentCard />
          <CommentCard />
          <CommentCard />
          </div>
        </Container>
      </Accordion.Collapse>
    </Accordion>
  )
}

export default PostCard;
//in ~/components/comment-card.js

import { Image} from 'react-bootstrap';

const CommentCard = () => {
  return (<div className="my-2 bg-light border border-secondary rounded-2 p-2">
      <div className="d-flex justify-content-start ">
        <Image src="https://picsum.photos/200/180" width={40} height={40} roundedCircle />
        <small className="mt-1 mx-2 p-1  text-truncate">
          <strong>
            8yu4983yhin8h983h4i4h983h89y4h984
          </strong>
        </small>
      </div>
      <div className="post-content text-justify p-2 m-1 mb-2">
        Laboris labore dolor est veniam, iudicem summis commodo, admodum labore eiusmod,noster e ut sint deserunt, litteris illustriora sed occaecat, litteris quid culpa admodum fore, quid ullamco a domesticarum.
      </div>
    </div>)
}

export default CommentCard;
//in ~/components/nav.js

import logo from '../logo.svg';
import {Navbar,Container} from 'react-bootstrap'

const Nav = function(){

  return (
    <Navbar bg="dark" variant="dark">
    <Container>
      <Navbar.Brand href="/">
        <img
          alt=""
          src={logo}
          width="30"
          height="30"
          className="d-inline-block align-top"
        />{' '}
        EMeet
      </Navbar.Brand>
    </Container>
  </Navbar>)
}
export default Nav;

We have small changes to make in our index.css

// in ~/index.css

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

.no-scrollbar::-webkit-scrollbar{
  display: none;
  scroll-padding: 5px;
}
.svg-style{
  position: absolute;
  top:40px;
  z-index: -3;
  height:14em;
  width:100%;
  transform: scale(1.5,3);
}

.scale-down:active{
  transform: scale(.95);
}

.post-content{
  display: -webkit-box;
  overflow: hidden;
  text-align: left;
  padding:4px;
  max-height: 80px;
  text-overflow: ellipsis;
}

You should have an interactive webpage. These are just boilerplates for the UI, we shall be making a lot of changes to them.

How Do We Connect React to the Contract/Wallet?

We will use web3.js library to connect our application to the wallet and smart contract.

Notes on Web3 Usage

Explore the web3 documentation here.

/*
Like in our test file, we have to call the contract's methods in order to
interact with the smart contract.
*/
//Let's get our contract
let contract = new web3.eth.Contract(jsonInterface, address);

//To call methods that read from the contract
contract.methods.yourMethod(parameter).call()

//To call methods that change state in the contract (writing to the contract)
contract.methods.yourMethod(parameter).send({from:<address>})

/*
You can see the difference between the two. You will use them shortly.
*/

In ~/client/src create a file getWeb3.js and add the following

// in ~/getWeb3.js

import Web3 from 'web3'

// The function returns a promise that resolves with a web3 instance if successful
const getWeb3 = () => new Promise((resolve, reject)=>{
  //Check if the browser is web3 enabled on loading
  window.addEventListener("load", async () => {

    if(window.ethereum){
      const web3 = new Web3(window.ethereum);
      try{
        //request access to the account
        await window.ethereum.enable();
        resolve(web3);
      }catch(err){
        reject(err);
      }
    }else{
      //if it is a legacy dapp browsers
      if(window.web3){
        const web3 = window.web3;
        console.log("injected web3 detected");
        resolve(web3);
      }else{
        //We fallback to the localhost
        const provider = new Web3.providers.HttpProvider("http://127.0.0.1:9545");
        const web3 = new Web3(provider);
        console.log("new web3 injected");
        resolve(web3);
      }
    }
  })
})

export default getWeb3;

In App.js

import getWeb3 from './getWeb3';
import EMeet from './contracts/EMeet.json';

//Create our application context with initial state 
//that will be shared among the components. For more info on 
//react useContext  https://reactjs.org/docs/hooks-reference.html#usecontext
export const AppContext = createContext({
  app:{
    web3:null,
    accounts: null,
    contract: null
  },
  setApp: () => {}
})

function App() {
  const [isLoading, setIsLoading] = useState(true);
  //our application state
  const [app, setApp] = useState({
    web3:null,
    accounts: null,
    contract: null
  });

  //context values
  let value = useMemo(()=>({app, setApp}),[app])


  async function init(){
    try {
      //get web3 instance and network
      const web3 = await getWeb3();

      //get accounts that we will use in this App
      let accounts = await web3.eth.getAccounts();

      //get the network id where our contract is running
      let networkId = await web3.eth.net.getId();
      let deployedNetwork =   EMeet.networks[networkId];
      let emeet =  new web3.eth.Contract(
        EMeet.abi,
        deployedNetwork && deployedNetwork.address
      );

      setApp({web3,accounts,contract:emeet});
      setIsLoading(false);
    } catch (error) {

      setIsLoading(false);
      // Catch any errors for any of the above operations.
      alert(
        `Failed to load web3, accounts, or contract. Check console for details.`,
      );
      console.error(error);
    }
  }

  useEffect(()=>{
    init();
  }, [isLoading])

  if(isLoading){
   //Displays growing loading icon when the app is connecting to the wallet
    return (<Container className="d-flex justify-content-center align-items-center" style={{paddingTop:"50vh"}}>
          <Spinner animation="grow" />
      </Container>)
  }

  return (
    <AppContext.Provider value={value}>
      <div>
        <Nav />
        <Container className="row m-auto mb-5" style={{zIndex:5}}>
          <ProfileContainer  />
          <PostContainer />
        </Container>
      </div>
    </AppContext.Provider>
  );
}

export default App;
Let's update the components

In profile-container.js

import {Col, Card} from 'react-bootstrap';
import Indenticon from 'react-identicons'
import {useContext} from 'react';
import {AppContext} from '../App'


const ProfileContainer = function(){
  const {app} = useContext(AppContext);
  let address = app.contract._address;

  return <Col lg="3" md="1" className="mt-4 me-2 d-none d-lg-block">
      <Card style={{ width: '18rem' }}>
       <div style={{width:200, height:200}} className="d-flex m-auto p-2">
          <Indenticon string={address} size={200} />
       </div>
        <Card.Body>
        <Card.Title>Your Details</Card.Title>
        <Card.Text>
          <strong>Address:</strong> {address}
        </Card.Text>
        </Card.Body>
      </Card>
        </Col>
}

export default ProfileContainer;

In post-container.js

import {Col, Form, Button} from 'react-bootstrap';
import PostCard from './post-card'
import {useContext, useEffect, useState} from 'react';
import {AppContext} from '../App'


const PostContainer = function(){
  const [posts, setPosts] = useState([])
  const [postContent, setPostContent] = useState("")

  const {contract, accounts} = useContext(AppContext).app;

  const loadPosts = async () => {

    let postNum, postsArr=[];
    postNum = await contract.methods.postCount().call();

    //get all posts from the contract (we have to loop through the post count)
     for(let i=1; i<=postNum; i++){
       let post  = await contract.methods.posts(i).call();
       postsArr.push(post)
     }
    //sort post according to the tip amount to provide high content for readers
    setPosts(postsArr.sort((a,b)=> b.tipAmount - a.tipAmount));
  }

  const publishPost = async (e) => {
    e.preventDefault()
    try {
      if(!postContent){
        return;
      }
      await contract.methods.createPost(postContent).send({from: accounts[0]});
      await loadPosts()
    } catch (e) {
      console.error(e)
    }
    setPostContent("")
  }
  useEffect(()=>{
    loadPosts()
  },[])


  return <Col lg="5" md="8" className="m-auto mt-4 mt-md-5 shadow-sm">
          <Form className="p-1" onSubmit={publishPost}>
            <Form.Group className="mb-3">
            <Form.Control as="textarea" rows={3} onChange={(e)=>setPostContent(e.target.value)} value={postContent}  style={{resize:"none"}} placeholder="What's on your mind?" />
            </Form.Group>
            <Button variant="primary scale-down" type="submit">Publish</Button>
          </Form>
          <div style={{maxHeight:"60vh",overflowY:"auto"}} className="mt-2 no-scrollbar">
            {!posts && <div>No post yet on the platform</div>}
             {posts&& posts.map((post, index)=> (<PostCard author={post.author} id={post.id} content={post.content}              tipAmount={post.tipAmount} key={index} />))}
          </div>
        </Col>
}

export default PostContainer;

In post-card.js

import {Form, Container, Button, Accordion, useAccordionButton} from 'react-bootstrap';
import CommentCard from './comment-card';
import Indenticon from 'react-identicons'
import {useState, useEffect} from 'react'
import {useContext} from 'react';
import {AppContext} from '../App'

//Toggles the comments section (Collapsed or Opened)
const PostCommentToggler = ({children, eventKey, callback}) => {
  const openOnClick = useAccordionButton(
    eventKey,
    () => callback && callback(eventKey),
  );
  return (<span className="ms-2 strong" onClick={openOnClick}> {children}</span>)
}



const PostCard = function({tipAmount:tip, author, content, id}){
  const [comments, setComments] = useState([])
  const [commentContent, setCommentContent] = useState("")
  const [tipAmount, setTipAmount] = useState(tip);
  const {contract, accounts, web3} = useContext(AppContext).app;

  const tipPost = async () => {
    try {
      const amount  = await web3.utils.toWei("0.1");
      await contract.methods.tipPost(id).send({from:accounts[0], value:amount})
      const result = await contract.methods.posts(id).call();
      setTipAmount(result.tipAmount);
      window.alert("You just tipped this post 0.1 ETH");
    } catch (e) {
      console.error(e)
    }
  }
  const loadComments = async () => {

    let commentsNum, commentsArr=[];

    commentsNum = await contract.methods.commentCount().call();

    for(let i=1; i<= commentsNum; i++){
      let comment = await contract.methods.comments(i).call();
      //push comments whose postId matches the id of the post
      if(comment.postId === id) commentsArr.push(comment);
    }
    setComments(commentsArr);
  }



  const publishComment = async (e) => {
    e.preventDefault()
    try {
      if(!commentContent){
        return;
      }

      await contract.methods.commentOnPost(id, commentContent).send({from: accounts[0]});

      //Refreshes the comment section of the post
      await loadComments();
    } catch (e) {
      console.error(e)
    }
    setCommentContent("")
  }

  useEffect(()=>{
    //loads the comments on mounting this comment card component
    loadComments();
  },[])


  return(
    <Accordion className="my-2 border border-secondary p-2">

        <div className="d-flex justify-content-start ">
        <div className="border rounded-circle d-flex justify-content-center align-items-center p-2" style={{width:43, height:43}}>
          <Indenticon string={author} size="34" />
        </div>
          <small className="mt-1 mx-2 p-1  text-truncate">
            <strong>
            {author}
            </strong>
          </small>
        </div>
        <div className="post-content text-justify p-2 m-1 mb-2">
        {content}
         </div>

       <div className="d-flex justify-content-between mt-2 w-100 px-2 border border-top-0 border-end-0 border-start-0 border-bottom-2 py-2 bg-light">
         <div>
          {web3.utils.fromWei(tipAmount)} ETH
          <span
          className="bg-primary badge text-light rounded py-1 px-2 ms-1 scale-down"
          onClick={tipPost}
          >
          Tip 0.1 ETH
          </span>
         </div>
         <div>
          {comments.length}
            <PostCommentToggler  eventKey="0">Comments</PostCommentToggler>
         </div>
      </div>
      <Accordion.Collapse eventKey="0">
        <Container className="mt-4">
          <Form className="p-1" onSubmit={publishComment}>
            <Form.Group className="mb-3">
            <Form.Control as="textarea" rows={2} onChange={(e)=>setCommentContent(e.target.value)} value={commentContent} style={{resize:"none"}} placeholder="This is interesting..." />
            </Form.Group>
            <Button variant="secondary text-white w-100 scale-down" type="submit">Post Comment</Button>
          </Form>
          <div style={{maxHeight:"250px",overflowY:"auto"}} className="mt-2 no-scrollbar">
          { comments ? comments.map((comment, index) => (<CommentCard content={comment.content} commenter={comment.commenter}  key={index} />))
          :(<div> Be the first to comment</div>)}
          </div>
        </Container>
      </Accordion.Collapse>
    </Accordion>
  )
}

export default PostCard;

In comment-card.js


import Indenticon from 'react-identicons'
// import {useContext} from 'react'

const CommentCard = ({commenter,content}) => {
  return (<div className="my-2 bg-light border border-secondary rounded-2 p-2">
      <div className="d-flex justify-content-start ">
        <Indenticon string={commenter} size="25" />
        <small className="mt-1 mx-2 p-1  text-truncate">
          <strong>
            {commenter}
          </strong>
        </small>
      </div>
      <div className="post-content text-justify p-2 m-1 mb-2">
      {content}
      </div>
    </div>)
}

export default CommentCard;

Most of the functions should be working properly at this point. To make our app a little less boring. Let's update App.js and index.css.

In App.js

...
//change it to this
return (
    <AppContext.Provider value={value}>
      <div style={{position:"relative", overflowX:"hidden", width:"100vw"}}>
        <svg className="svg-style" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="#adb5bd" fillOpacity="1" d="M0,192L17.1,176C34.3,160,69,128,103,128C137.1,128,171,160,206,165.3C240,171,274,149,309,170.7C342.9,192,377,256,411,272C445.7,288,480,256,514,234.7C548.6,213,583,203,617,170.7C651.4,139,686,85,720,58.7C754.3,32,789,32,823,69.3C857.1,107,891,181,926,197.3C960,213,994,171,1029,176C1062.9,181,1097,235,1131,234.7C1165.7,235,1200,181,1234,160C1268.6,139,1303,149,1337,170.7C1371.4,192,1406,224,1423,240L1440,256L1440,0L1422.9,0C1405.7,0,1371,0,1337,0C1302.9,0,1269,0,1234,0C1200,0,1166,0,1131,0C1097.1,0,1063,0,1029,0C994.3,0,960,0,926,0C891.4,0,857,0,823,0C788.6,0,754,0,720,0C685.7,0,651,0,617,0C582.9,0,549,0,514,0C480,0,446,0,411,0C377.1,0,343,0,309,0C274.3,0,240,0,206,0C171.4,0,137,0,103,0C68.6,0,34,0,17,0L0,0Z"></path></svg>
        <Nav />
        <Container className="row m-auto mb-5" style={{zIndex:5}}>
          <ProfileContainer  />
          <PostContainer />
        </Container>
      </div>
    </AppContext.Provider>
  );
}
...

And in index.css add

...
.identicon{
  margin: auto;
}
...

Now that you are done, play around with the app and show your friend. Congratulations on reaching this end.

End Result

Capture.PNG

Or view the video here (muted, very important)