Learn React
With Austin @bitcoinplebdev
Visit the slides
Topics covered in this lesson
What we learned in development environments
React from a high level
At its very core, React basically maintains a tree for you. This tree is able to do efficient diff computations on DOM elements.
Think of your HTML code as a tree. In fact, that is exactly how the browser treats your DOM (your rendered HTML on the browser). React allows you to effectively re-construct your DOM in JavaScript and push only those changes to the DOM which have actually occurred.
React from a high level
Virtual DOM in React is a “virtual” representation of the actual DOM. It is nothing but an object created to replicate the actual DOM.
Unlike the actual DOM, the virtual DOM is cheap to create because it doesn’t write to the screen. It is only available as a strategy to prevent a redraw of unnecessary page elements during re-render.
React components
Components are independent and reusable bits of code. They serve the same purpose as JavaScript functions, but work in isolation and return HTML.
Put simply, React components are functions that may or may not house logic, but ALWAYS return some UI elements.
Components come in two types, Class components and Function components
React class components
Before React 16.8, Class components were the only way to track state and lifecycle on a React component. Function components were considered "state-less".
With the addition of Hooks, Function components are now almost equivalent to Class components. The differences are so minor that you will probably never need to use a Class component in React.
React functional components
As you can see below a functional component is just a standard JavaScript function that returns UI elements
Notice that we are using the JavaScript arrow function syntax for our functions
React component hierarchy
React allows us to break the user interface into logically connected components. When combined these components make a complete application structure.
These components always fall into a hierarchy nested within the primary App component.
When building out components you want to make them reusable while still keeping the minimal amount of logic and functionality in each one
React component hierarchy
src/components/Header.js
src/App.js
Passing props in React
“Props” is the generic term for any data that is passed down to child components.
Props can be passed in with a props object or “destructured” by defining the specific prop name
Passing props in React
Props can only be passed down to child components in the hierarchy, they cannot be passed from a child component to a parent component.
So if we were fetching data from an API that was going to be displayed in the red, purple, and green components we would need to fetch that data in the orange component and pass it down.
�Otherwise we would have to fetch the data three different times in each of the child components
React hooks
Hooks are built-in React functions introduced in React version 16.8. They allow you to use features of the React library like lifecycle methods, state, and context in functional components without having to worry about rewriting it to a class.
Each React Hook name is prefixed with the word "use". For example, useState or useEffect. This format was chosen because Hooks let developers use the special features of the React library. So you're using that special feature of the React library.
useState hook
The useState Hook allows you to create, update, and manipulate state inside functional components.�React has this concept of state, which are variables that hold data that our components depend on and may change over time. Whenever these variables change, React updates the UI by re-rendering the component in the DOM with the current values of the state variables.
useEffect hook
The Effect Hook, just like the name implies, carries out an effect each time there is a state change. By default, it runs after the first render and every time the state is updated. ��In this example, we create a state variable count with an initial value of zero. A button in the DOM will increase the value of this variable by one every time it is clicked. The useEffect hook will run every time the count variable changes and then log out some information to the console.
useEffect hook
The dependency array is an optional value we can pass as the second argument into our useEffect hook. ��What is in the dependency array will determine when the effect will run
Understanding callback functions
Why callback functions
A callback is just a function that's passed into another function, with the expectation that the callback will be called at the appropriate time.
Callback functions are mostly used in areas where the top level “higher order” function is doing an operation that takes an uncertain amount of time so by passing in a callback we can ensure that it is only called once the higher order functions finishes it’s thing
Let’s build pleb wallet in React
Finished project: https://github.com/AustinKelsay/learn-react
Add in the HTML for App.js
import React from "react";
import "./App.css";
function App() {
return (
<div className="App">
<header>
<h1>pleb wallet</h1>
</header>
{/* <Buttons /> */}
<div className="row">
<div className="balance-card">
<h2>Balance</h2>
{/* <p>{balance} sats</p> */}
</div>
<div className="balance-card">
<h2>Price</h2>
{/* <p>${price}</p> */}
</div>
</div>
<div className="row">
<div className="row-item">
{/* <Transactions transactions={transactions} /> */}
</div>
<div className="row-item">{/* <Chart chartData={chartData} /> */}</div>
</div>
<footer>
<p>Made by plebs, for plebs.</p>
</footer>
</div>
);
}
export default App;
Add in the CSS for App.css
body {
background-color: #192734;
font-family: monospace;
}
header {
border-bottom: 2px solid #ffbf46;
}
h1 {
text-align: center;
color: #8a4fff;
}
img {
width: 100%;
height: 100%;
}
footer {
border-top: 2px solid #ffbf46;
padding: 1%;
text-align: center;
position: fixed;
bottom: 0;
left: 0;
right: 0;
}
footer p {
color: #8a4fff;
}
.balance-card {
background-color: #ffbf46;
border: 2px solid #8a4fff;
padding: 1%;
width: 25%;
margin-top: 3%;
margin-bottom: 1%;
border-radius: 5px;
}
.balance-card p {
font-size: 1.2rem;
font-weight: bold;
}
.row {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.row-item {
background-color: ghostwhite;
border: 2px solid #8a4fff;
border-radius: 5px;
height: 300px;
width: 40%;
overflow: scroll;
}
.row-item h3 {
padding-left: 2%;
}
.row-item p {
padding-left: 2%;
}
.transaction .transaction-amount {
padding-top: 1%;
}
.transaction-amount {
border-bottom: 1px solid black;
}
@media (max-width: 876px) {
.row-item {
width: 100%;
height: 200px;
}
.balance-card {
width: 100%;
text-align: center;
margin-right: 2%;
margin-left: 2%;
}
}
@media (max-width: 615px) {
.row {
flex-direction: column;
}
.row-item {
margin-top: 1%;
}
.balance-card {
width: 80%;
margin: 0 auto;
margin-top: 1%;
}
}
Add the getPrice function in App.js
import React, { useEffect, useState } from "react";
import axios from "axios";
import "./App.css";
function App() {
const [price, setPrice] = useState(null);
const getPrice = () => {
// Axios is a library that makes it easy to make http requests
// After we make a request, we can use the .then() method to handle the response asynchronously
// This is an alternative to using async/await
axios
.get("https://api.coinbase.com/v2/prices/BTC-USD/spot")
// .then is a promise that will run when the API call is successful
.then((res) => {
setPrice(res.data.data.amount);
})
// .catch is a promise that will run if the API call fails
.catch((err) => {
console.log(err);
});
};
// useEffect is a 'hook' or special function that will run code based on a trigger
// The brackets hold the trigger that determines when the code inside of useEffect will run
// Since it is empty [] that means this code will run once on page load
useEffect(() => {
getPrice();
}, []);
Uncomment the <p> tag with price in it
<div className="balance-card">
<h2>Price</h2>
{/* <p>${price}</p> */}
</div>
<div className="balance-card">
<h2>Price</h2>
<p>${price}</p>
</div>
Setup useEffect to get price every 5 seconds
useEffect(() => {
// setInterval will run whatever is in the callback function every 5 seconds
const interval = setInterval(() => {
getPrice();
}, 5000);
return () => clearInterval(interval);
}, []);
Create a function to get our wallet balance
const [balance, setBalance] = useState(null);
const getWalletBalance = () => {
// ToDo: Lookup how to move the X-API-Key to a .env file to keep it secret for when we push to Github
const headers = {
"X-Api-Key": "52cac212fc664da393ac45df991fdb84",
};
axios
.get("https://legend.lnbits.com/api/v1/wallet", { headers })
.then((res) => {
// Divide our balance by 1000 since it is denominated in millisats
setBalance(res.data.balance / 1000);
})
.catch((err) => console.log(err));
};
Add this function into our useEffect hooks
useEffect(() => {
getPrice();
getWalletBalance();
}, []);
// Run these functions every 5 seconds after initial page load
useEffect(() => {
const interval = setInterval(() => {
getPrice();
getWalletBalance();
}, 5000);
return () => clearInterval(interval);
}, []);
Uncomment the <p> tag with balance in it
<div className="balance-card">
<h2>Balance</h2>
{/* <p>{balance} sats</p> */}
</div>
<div className="balance-card">
<h2>Balance</h2>
<p>{balance} sats</p>
</div>
Create a components folder and add the Transactions JS and CSS files in it
Create Transactions.js with hardcoded data
import React from "react";
import "./Transactions.css";
export const Transactions = () => {
const transactions = [
{
checking_id: 1,
amount: 1000,
time: 1669064951,
bolt11: "lnbc20n1p3h24fhsp590ccjwy...",
},
{
checking_id: 2,
amount: 2000,
time: 1668035952,
bolt11: "lnbc980n1p3h24fhsp5cpcjdw...",
},
];
// ToDo: Improve tx parsing to display internal payments, incomplete payments, and further verify the transactions we are listing out
const parseTx = (tx) => {
// turn unix timestamp into a date
// Todo: format date further to include hours, minutes, and seconds
const date = new Date(tx.time * 1000);
const formattedDate = date.toLocaleDateString("en-US");
return (
<div key={tx.checking_id} className="tx-item">
<p>Received from {tx.bolt11.substring(0, 25)}...</p>
<p>+{tx.amount / 1000} sats</p>
<p className="transaction-date">{formattedDate}</p>
</div>
);
};
return (
<div>
<h3>Transactions</h3>
{transactions.map((transaction) => {
return parseTx(transaction);
})}
</div>
);
};
export default Transactions;
Add the styles in Transactions.css
h3 {
padding-left: 2%;
}
p {
padding-left: 2%;
padding-top: 1%;
}
.transaction-date {
border-bottom: 1px solid black;
}
Import / uncomment the Transactions component in App.js (and remove the “transactions” prop for right now)
import Transactions from "./components/Transactions";
TOP of our file
<div className="row-item">
{/* <Transactions transactions={transactions} /> */}
</div>
<div className="row-item">
<Transactions />
</div>
Create a function to get our real transactions
const [transactions, setTransactions] = useState([]);
const getTransactions = () => {
// ToDo: Lookup how to move the X-API-Key to a .env file to keep it secret for when we push to Github
const headers = {
"X-Api-Key": "52cac212fc664da393ac45df991fdb84",
};
axios
.get("https://legend.lnbits.com/api/v1/payments", { headers })
.then((res) => {
setTransactions(res.data);
})
.catch((err) => console.log(err));
};
Add getTransactions to our useEffects
useEffect(() => {
getPrice();
getWalletBalance();
getTransactions();
}, []);
// Run these functions every 5 seconds after initial page load
useEffect(() => {
// setInterval will run whatever is in the callback function every 5 seconds
const interval = setInterval(() => {
getPrice();
getWalletBalance();
getTransactions();
}, 5000);
return () => clearInterval(interval);
}, []);
Pass our transactions array as a prop into our Transactions component
<div className="row-item">
<Transactions transactions={transactions} />
</div>
In App.js return
In Transactions.js
export const Transactions = ({transactions}) => {
// const transactions = [
// {
// checking_id: 1,
// amount: 1000,
// time: 1669064951,
// bolt11: "lnbc20n1p3h24fhsp590ccjwy...",
// },
// {
// checking_id: 2,
// amount: 2000,
// time: 1668035952,
// bolt11: "lnbc980n1p3h24fhsp5cpcjdw...",
// },
// ];
Complete our parseTx function
// ToDo: Improve tx parsing to display internal payments, incomplete payments, and further verify the transactions we are listing out
const parseTx = (tx) => {
// turn unix timestamp into a date
// Todo: format date further to include hours, minutes, and seconds
const date = new Date(tx.time * 1000);
const formattedDate = date.toLocaleDateString("en-US");
// ToDo: Handle pending payments since we are currently ignoring them and not displaying them on our past transactions list
if (tx.pending) return null;
if (tx.amount > 0) {
return (
<div key={tx.checking_id} className="tx-item">
<p>Received from {tx.bolt11.substring(0, 25)}...</p>
<p>+{tx.amount / 1000} sats</p>
<p className="transaction-date">{formattedDate}</p>
</div>
);
}
if (tx.amount < 0) {
return (
<div id={tx.checking_id} key={tx.checking_id} className="tx-item">
<p>Sent with {tx.bolt11.substring(0, 25)}...</p>
<p className="tx-amount">{tx.amount / 1000} sats</p>
<p className="transaction-date">{formattedDate}</p>
</div>
);
}
};
Add “Buttons.js” and “Buttons.css” files into your components folder
Add in the basic Buttons component layout in Buttons.js
import React, { useState } from "react";
import "./Buttons.css";
export const Buttons = () => {
return (
<div>
<div className="buttons">
<button
className="button"
onClick={() =>
console.log("This button will open a modal to send a payment")
}
>
Send
</button>
<button
className="button"
onClick={() =>
console.log("This button will open a modal to receive a payment")
}
>
Receive
</button>
</div>
{/* <PaymentsModal modalState={modalState} setModalState={setModalState} /> */}
</div>
);
};
export default Buttons;
Add in the CSS for Buttons.css
label {
padding-bottom: 2%;
}
.buttons {
width: 50%;
margin: 0 auto;
margin-top: 3%;
display: flex;
justify-content: space-around;
}
.button {
background-color: #ffbf46;
border: 2px solid #8a4fff;
border-radius: 5px;
padding: 5px;
font-size: 1.2rem;
font-family: monospace;
font-weight: bold;
width: 100px;
}
.buttons button:hover {
cursor: pointer;
opacity: 0.6;
}
Import the Buttons component into App.js and uncomment it in the return
import Buttons from "./components/Buttons";
<div className="App">
<header>
<h1>pleb wallet</h1>
</header>
{/* <Buttons /> */}
<div className="row">
<div className="App">
<header>
<h1>pleb wallet</h1>
</header>
<Buttons />
<div className="row">
Install the modal library we will be using from NPM
Create PaymentsModal.js and PaymentsModal.css files
Add in the basic PaymentsModal component
import React, { useState } from "react";
import Modal from "react-modal";
import axios from "axios";
import "./PaymentsModal.css";
const customStyles = {
content: {
top: "20%",
left: "40%",
right: "40%",
bottom: "auto",
},
};
const PaymentsModal = ({ modalState, setModalState }) => {
return (
<Modal
isOpen={modalState.open}
style={customStyles}
contentLabel="Example Modal"
appElement={document.getElementById("root")}
>
<p
className="close-button"
onClick={() => {
setModalState({ open: false, type: null });
}}
>
X
</p>
<p>Here's our modal!</p>
</Modal>
);
};
export default PaymentsModal;
Add in the styles for PaymentsModal.css
form {
margin: 2% auto;
display: flex;
flex-direction: column;
text-align: center;
}
form {
width: 100%;
}
form .button {
margin: 10% auto;
}
section h3 {
text-align: center;
}
section p {
word-wrap: break-word;
}
.close-button {
cursor: pointer;
text-align: end;
}
Add in a useState hook for the modal state and uncomment the PaymentsModal component passing in the state and setter
import PaymentsModal from "./PaymentsModal";
...
const [modalState, setModalState] = useState({
type: "",
open: false,
});
...
</div>
{/* <PaymentsModal modalState={modalState} setModalState={setModalState} /> */}
</div>
</div>
<PaymentsModal modalState={modalState} setModalState={setModalState} />
</div>
Add in the state for our PaymentsModal and some placeholder functions for sending/receiving
// Our state for the info we will send to either generate a new invoice or pay an invoice
const [formData, setFormData] = useState({
amount: 0,
invoiceToPay: "",
});
// Our state for storing the invoice we created to be paid
const [invoice, setInvoice] = useState("");
// Our state for the invoice we paid
const [paymentInfo, setPaymentInfo] = useState({
paymentHash: "",
checkingId: "",
});
const handleSend = () => {};
const handleReceive = () => {};
Add in the forms/logic for our modal
<Modal
isOpen={modalState.open}
style={customStyles}
contentLabel="Example Modal"
appElement={document.getElementById("root")}
>
<p
className="close-button"
onClick={() => {
setModalState({ open: false, type: null });
}}
>
X
</p>
{/* If it is a send */}
{modalState.type === "send" && (
<form>
<label>paste an invoice</label>
<input
type="text"
value={formData.invoiceToPay}
onChange={(e) =>
setFormData({ ...formData, invoiceToPay: e.target.value })
}
/>
<button className="button" onClick={(e) => handleSend(e)}>
Submit
</button>
</form>
)}
{/* If it is a receive */}
{modalState.type === "receive" && (
<form>
<label>enter amount</label>
<input
type="number"
min="0"
value={formData.amount}
onChange={(e) =>
setFormData({ ...formData, amount: e.target.value })
}
/>
<button className="button" onClick={(e) => handleReceive(e)}>
Submit
</button>
</form>
)}
{/* If we are displaying our newly created invoice */}
{invoice && (
<section>
<h3>Invoice created</h3>
<p>{invoice}</p>
{/* ToDo: Create a QR code out of this invoice as well */}
</section>
)}
{/* If we are displaying the status of our successful payment */}
{paymentInfo.paymentHash && (
<section>
<h3>Payment sent</h3>
<p>Payment hash: {paymentInfo.paymentHash}</p>
<p>Checking id: {paymentInfo.checkingId}</p>
</section>
)}
</Modal>
Fill in the handleSend function
const handleSend = (e) => {
// Keep the page from refreshing when the form is submitted
e.preventDefault();
const headers = {
"X-Api-Key": "52cac212fc664da393ac45df991fdb84",
};
const data = {
bolt11: formData.invoiceToPay,
out: true,
};
axios
.post("https://legend.lnbits.com/api/v1/payments", data, { headers })
.then((res) =>
setPaymentInfo({
paymentHash: res.data.payment_hash,
checkingId: res.data.checking_id,
})
)
.catch((err) => console.log(err));
return;
};
Fill in the handleReceive function
const handleReceive = (e) => {
// Keep the page from refreshing when the form is submitted
e.preventDefault();
const headers = {
"X-Api-Key": "52cac212fc664da393ac45df991fdb84",
};
const data = {
amount: formData.amount,
out: false,
// ToDo: Add additional form for user to be able to customize the memo
memo: "LNBits",
};
axios
.post("https://legend.lnbits.com/api/v1/payments", data, { headers })
.then((res) => setInvoice(res.data.payment_request))
.catch((err) => console.log(err));
return;
};
Add a function to clear the formState
// Function to clear all of our state when we close the modal
const clearForms = () => {
setModalState({
type: "",
open: false,
});
setInvoice("");
setPaymentInfo({
paymentHash: "",
checkingId: "",
});
setFormData({
amount: 0,
invoiceToPay: "",
});
};
...
<p
className="close-button"
onClick={() => {
clearForms();
}}
>
X
</p>
Create Chart.js and Chart.css files
Install the chart library we will be using from NPM
Add in the chart component
import React from "react";
import LineChart from "react-linechart";
import "../../node_modules/react-linechart/dist/styles.css";
import "./Chart.css";
const Chart = ({ chartData }) => {
if (!chartData && !chartData?.length) return null;
const data = [
{
color: "steelblue",
points: chartData,
},
];
return (
<div className="chart-container">
{chartData && chartData.length <= 1 ? (
// ToDo: Add a loading spinner
<p>Loading</p>
) : (
<LineChart
xLabel="Time"
height={300}
// ToDo: Customize width to be responsive based on screen size
width={550}
data={data}
onPointHover={(obj) => `price: $${obj.y}<br />time: ${obj.x}`}
ticks={4}
hideYLabel={true}
hideXLabel={true}
xDisplay={(timestamp) =>
new Date(timestamp).toLocaleTimeString("en-US")
}
/>
)}
</div>
);
};
export default Chart;
Add in our 1 style in Chart.css
.chart-container {
padding-left: 1%;
}
Add updateChartData function and chartData state into App.js
const [chartData, setChartData] = useState(null);
const updateChartData = (currentPrice) => {
const timestamp = Date.now();
// We are able to grab the previous state to look at it and do logic before adding new data to it
setChartData((prevState) => {
// If we have no previous state, create a new array with the new price data
if (!prevState)
return [
{
x: timestamp,
y: Number(currentPrice),
},
];
// If the timestamp or price has not changed, we don't want to add a new point
if (
prevState[prevState.length - 1].x === timestamp ||
prevState[prevState.length - 1].y === Number(currentPrice)
)
return prevState;
// If we have previous state than keep it and add the new price data to the end of the array
return [
// Here we use the "spread operator" to copy the previous state
...prevState,
{
x: timestamp,
y: Number(currentPrice),
},
];
});
};
Pass each updated price into setChartData from the getPrice function
const getPrice = () => {
// Axios is a library that makes it easy to make http requests
// After we make a request, we can use the .then() method to handle the response asynchronously
// This is an alternative to using async/await
axios
.get("https://api.coinbase.com/v2/prices/BTC-USD/spot")
// .then is a promise that will run when the API call is successful
.then((res) => {
setPrice(res.data.data.amount);
updateChartData(res.data.data.amount);
})
// .catch is a promise that will run if the API call fails
.catch((err) => {
console.log(err);
});
};
Import the chart component in App.js and uncomment it in the return
import Chart from "./components/Chart";
...
<div className="row-item">{/* <Chart chartData={chartData} /> */}</div>
<div className="row-item">
<Chart chartData={chartData} />
</div>
Pleb wallet should now be working!
Push your work to Github now so vercel can update and deploy our new build
React resources
Get in contact with me
Twitter @bitcoinplebdev