Just Enough React (and Next.js) for Web Development
Day 10 of my 100DaysOfCode
Photo by Lautaro Andreani on Unsplash
๐ค What happens when you click on the Increment button?
In the 100daysofcode series, we built a very simple dApp that used HTML, CSS, and some Javascript. In the real world, however, these type of 'vanilla' website implementations are a thing of the past.
Today, we use web frameworks to simplify the web development process. Is it simpler though? Depends on whose perspective you're looking from. If you're just starting out and have never done this before, it may take some time till you understand all the necessary concepts. But, in the long run, you will thank yourself and be happy that you took the time to learn it.
The biggest and most common web frameworks used today are:
While they each have their own pros and cons, React has by far taken the web development world by storm. The same is true in the Web3 space, where React is the most commonly used web framework for building dApps. Throughout the Sophomore track, and all later tracks as well, we will be using a lot of React, so think of this level as a React crash course which will teach you just enough to get started. This is not meant to be a replacement for learning React on platforms that focus on teaching Web2 but is to be used as a guide to understanding just how much is necessary to get started so you don't get stuck in tutorial hell. ๐
Actually, we will be using Next.js - which is an extension of React itself - but more on that later. Let's learn some React first.
WTF is React?
React is a web framework that makes it easy to build and reason about the 'view' of your web app. The 'view' is what is displayed on the screen, how it changes, how it updates, etc. React essentially just gives you a template language, and you can create Javascript functions that return HTML.
Normal Javascript functions return Javascript-esque things - strings, numbers, booleans, objects, etc. React basically combined Javascript and HTML to produce a language they call JSX. In JSX, Javascript-like functions return HTML instead of regular Javascript things. That's basically it.
The combination of Javascript functions that return HTML are called Components. Components are written in JSX. Though seemingly awkward at first, they are actually quite easy to work with once you get used to it.
Here's an example of a simple component:
function ShoppingList() { return ( <div className="shopping-list"> <h1>Shopping List</h1> <ul> <li>Apples</li> <li>Bananas</li> <li>Grapes</li> </ul> </div> );}
Great, but this isn't much different from HTML. But what if you wanted to render a list of items based on an array?
function ShoppingList() { const items = ["Apples", "Bananas", "Grapes"];
return ( <div className="shopping-list"> <h1>Shopping List</h1> <ul> {items.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> </div> );}
Wow, look at that! We just used Javascript inside HTML. In JSX, you can write Javascript inside HTML by wrapping the JS code in curly braces {
and }
. Actually though, if you think about this a little more, you will realize what is happening. .map()
is a Javascript function, which loops over an array and returns something for each item. In this case, it is looping over the items
array and returning an li
element, that has the value of the Javascript variable item
within it, which is HTML. Get it? ๐ค
Inside our component, we basically embedded another component. The map function is a JS function that returns HTML. It is a component. Even though it is not explicitly defined as a top-level function, it is still a component.
Embedding components inside other components is the power of React. This is called Composition. We are composing together multiple Javascript functions that return HTML, and building up a single combined HTML document from it that will be displayed on the web app.
Data Passing between Components
Components are not very useful if they are just static. Sure, looping over arrays and things are fine, but most web apps today are not static documents. Most web apps today will dynamically fetch data from some sort of server, database, or blockchain. This means the same component will often times be required to display different data.
The main use case of having components is being able to write code that is reusable and can, within it, contain different information without rewriting the entire code again.
Let's take a look at an example. Which of these two codes is more readable?
<div class="cards"> <div class="card"> <img src="img_avatar.png" alt="Avatar" /> <div class="container"> <h4><b>Alice</b></h4> <p>Frontend Developer</p> </div> </div>
<div class="card"> <img src="img_avatar.png" alt="Avatar" /> <div class="container"> <h4><b>Bob</b></h4> <p>Backend Developer</p> </div> </div>
<div class="card"> <img src="img_avatar.png" alt="Avatar" /> <div class="container"> <h4><b>Charlie</b></h4> <p>Full Stack Developer</p> </div> </div></div>
function Cards() { return ( <div className="cards"> <!--Data is passed to children through HTML attributes--> <Card name="Alice" job="Frontend Developer" /> <Card name="Bob" job="Backend Developer" /> <Card name="Charlie" job="Full Stack Developer" /> </div> )}
// Card receives an object as an argument// We can destructure the object to get specific variables// from inside the object - name and jobfunction Card({name, job}) { return ( <div className="card"> <img src="img_avatar.png" /> <div className="container"> <h4><b>{name}</b></h4> <p>{job}</p> </div> </div> )}
The plain HTML example reused the same code three times, even though the only thing that actually changed was just the name of the person and their job title.
In JSX, we can abstract away each Card
as a component, which takes certain data from its parent component (in this case, Cards
). Parent component passes data to the child through HTML-like attributes (name="Alice"
), and the child accesses their values like a JS function receiving a JS object as an argument. The Card
component can then return HTML with variable data inside it depending on what it received from the parent.
๐ค You can pass arbitrary data to components through props?
Yes
No
This code is much more easily reusable and extendable. Want to slightly change how all the cards look? Just modify a single component! Not all the copy-pasted HTML.
Interactive Components
Alright, so we can now pass data between components. That is all good, but we haven't yet added interactivity. Things like being able to run some code when a button is clicked, or when text is typed into an input box, etc.
Thankfully, in Javascript, functions can contain functions within them. For example,
function someFunc() { function otherFunc() { console.log("Hello!"); }
otherFunc();}
someFunc(); // will print "Hello!"
otherFunc(); // will throw an error! undefined here
otherFunc
is only available for use inside the someFunc
itself. This is a rarely used feature in regular Javascript but is very heavily used when working with React. Let's see why with an example.
function Button() { function handleClick() { console.log("Hello"); }
return ( <button className="button" onClick={handleClick}> Click Me! </button> );}
We have a JSX Function called Button
. Within this function, we have another function called handleClick
. In the HTML <button>
tag, we specify onClick={handleClick}
that when the button is clicked, the handleClick
function is called. This function is only available inside the Button
component. Clicking the button on the web app will print Hello
in the browser console. This is how we build interactive websites with React!
That example is still fairly simple because handleClick
takes no arguments. What if we want to print text in the console as the user is typing into an input box? How do we pass the text to the function?
Here's how.
function PrintTypedText() { function handleOnChange(text) { console.log(text); }
return <input type="text" onChange={(e) => handleOnChange(e.target.value)} />;}
The HTML input
element provides a handy event listener - onChange
- that is triggered every time the text in that input box changes (typing a new character, deleting a character, etc.).
Along with triggering a function though, it also passes along the HTML element that changed (referred to as e
here). We can then take the HTML element e
and extract the text using e.target
.value
and pass it as an argument to handleOnChange
which will log the text to the browser console.
Different HTML elements have different event handlers - onChange
and onClick
are two examples, but there are many more! You can find a list of all HTML events here.
By combining HTML events with function handlers, we can do all sorts of cool things! Load data from a server, send data to a server, update our view, etc.
React Hooks - useState and useEffect
Alright, we have gone over Composition, Data Passing, and Interactivity. But, our apps are still quite dumb. Interactivity will allow you to run some code on button clicks etc. but what if you want to update some variables?
Unfortunately, the following does not work
function DoesNotWork() { let myNumber = 0;
function increment() { myNumber++; }
return ( <div> <p>{myNumber}</p> <button onClick={increment}>Increment!</button> </div> );}
No matter how many times you click the Increment
button, the displayed number on the screen will be stuck at 0
. This is because when you update regular variables like myNumber
from within a React component, even though the value is updated, React does not actually re-render the view of the web app. It does not automatically update the HTML view of the page.
React Hooks are functions that 'hook' into different parts of React Components allowing you to do things like updating the view when a variable's value is changed, or running some JS code automatically every time the page is loaded or a variable is changed, and many more cool things! We will primarily focus on three React hooks which are used 95% of the time - useState
, useEffect
, and useRef
.
useState
There are a lot of times when you want the HTML view to update based on some variable's value changing. We can use the useState
hook to maintain a variable which automatically re-renders the HTML displayed on the screen every time its value is changed. Here's an example:
function ThisWorks() { // myNumber is the variable itself // setMyNumber is a function that lets us update the value // useState(0) initializes the React Hook // with the starting value of 0 const [myNumber, setMyNumber] = useState(0);
function increment() { // Sets the new value to the old value + 1 setMyNumber(myNumber + 1); }
return ( <div> <p>{myNumber}</p> <button onClick={increment}>Increment!</button> </div> );}
If you try to run the above code, you will see the view of the web app automatically gets updated to reflect the new value of the variable.
Variables created using useState
in React are called State Variables. State Variables can be updated and automatically update the view of the app. Here's another example of using State Variables with input boxes.
function StateWithInput() { // myName is the variable // setMyName is the updater function // Create a state variable with initial value // being an empty string "" const [myName, setMyName] = useState("");
function handleOnChange(text) { setMyName(text); }
return ( <div> <input type="text" onChange={(e) => handleOnChange(e.target.value)} /> <p>Hello, {myName}!</p> </div> );}
We see the text displayed on the HTML changes as the content of the input box changes. Great!
The last thing I want to say about useState is that you can also use it for more than just basic types like strings and numbers. You can also use them to store arrays and objects. However, there is a caveat here. Let's look at an example:
function StateArrayDoesNotWork() { const [fruits, setFruits] = useState([]); const [currentFruit, setCurrentFruit] = useState("");
function updateCurrentFruit(text) { setCurrentFruit(text); }
function addFruitToArray() { fruits.push(currentFruit); }
return ( <div> <input type="text" onChange={(e) => updateCurrentFruit(e.target.value)} /> <button onClick={addFruitToArray}>Add Fruit</button>
<ul> {fruits.map((fruit, index) => ( <li key={index}>{fruit}</li> ))} </ul> </div> );}
If you try to run this, you will see no fruits get displayed on the screen. Also, notice that we are not using the setFruits
function anywhere, instead of just trying to .push
the fruits
array.
When we try to update the array directly, React does not register the state change, and that is also considered an invalid state update which can cause the application to behave unexpectedly. We know we need to somehow use setFruits
, but how? The answer is that we actually need to create a copy of the fruits array, add the fruit to it, and set the state variable to be the new array entirely. Example below:
function StateArray() { const [fruits, setFruits] = useState([]); const [currentFruit, setCurrentFruit] = useState("");
function updateCurrentFruit(text) { setCurrentFruit(text); }
function addFruitToArray() { // The spread operator `...fruits` adds all elements // from the `fruits` array to the `newFruits` array // and then we add the `currentFruit` to the array as well const newFruits = [...fruits, currentFruit]; setFruits(newFruits); }
return ( <div> <input type="text" onChange={(e) => updateCurrentFruit(e.target.value)} /> <button onClick={addFruitToArray}>Add Fruit</button>
<ul> {fruits.map((fruit, index) => ( <li key={index}>{fruit}</li> ))} </ul> </div> );}
If you try to run the above code, you will see it works as expected. Every time you press the button, the current text from the input box is added into the array, which causes the list of fruits displayed on the HTML to be updated. You can keep adding as many fruits as you would like!
The same is true for Objects as well. If your state variable contains an object, you need to create a copy of the object first, update a value, and then set the state variable to the new object entirely.
useEffect
So we can manage state now, that's great! State changes also affect our rendered HTML, also great!
But, oftentimes, there is a need to automatically run some code when the page is first loaded - perhaps to fetch data from a server or the blockchain - and also the need to automatically run some code when a certain state variable changes.
These types of functions are called side effects. React gives us the useEffect
hook which allows us to write these kinds of effects. useEffect
takes two arguments - a function and a dependency array. The function is the code that runs when the effect is run, and the dependency array specifies when to trigger the side effect.
Consider an example where when the website is first loaded, it wants to load some data from the server. While doing so, it wants to display the user a loading screen, and then once the data has been loaded, remove the loading screen and show the actual content. How do we do this?
function LoadDataFromServer() { // Create a state variable to hold the data returned from the server const [data, setData] = useState(""); // Create a state variable to maintain loading state const [loading, setLoading] = useState(false);
async function loadData() { // Set `loading` to `true` until API call returns a response setLoading(true);
// Imaginary function that performs an API call to load // data from a server const data = await apiCall(); setData(data);
// We have the data, set `loading` to `false` setLoading(false); }
// loadData is the function that is run // An empty dependency array means this code is run // once when the page loads useEffect(() => { loadData(); }, []);
// Display `"Loading..."` while `loading` is `true`, // otherwise display `data` return <div>{loading ? "Loading..." : data}</div>;}
If you run the above code from the link, you will see it displays Loading...
for 5 seconds on the screen and then displays ABCDEF
. It's because apiCall
is a function that just waits for 5 seconds and then returns the string ABCDEF
.
The useEffect
calls loadData
when the page is first loaded - thanks to the empty dependency array - and the state variables make the HTML render the appropriate content.
This is good for running code when the page first loads, but what about running some piece of code every time a state variable's value changes? For example, when you're searching for a person's name on Facebook, how does Facebook fetch and display recommendations every time you add/remove a character?
You can also do that with useEffect
, by providing the state variable in the dependency array. Every time the value of that variable changes, the effect will be run.
function DependentEffect() { const names = ["Alice", "Bob", "Charlie", "David", "Emily"];
const [recommendations, setRecommendations] = useState([]); const [searchText, setSearchText] = useState("");
useEffect(() => { // If user is not searching for anything, don't show any recomendations if (searchText.length === 0) { setRecommendations([]); } // Else, find recommendations else if (searchText.length > 0) { const newRecs = names.filter((name) => name.toLowerCase().includes(searchText.toLowerCase()) ); setRecommendations(newRecs); } }, [searchText]);
return ( <div> <input type="text" onChange={(e) => setSearchText(e.target.value)} /> <h2>Recommendations:</h2> <ul> {recommendations.map((rec, index) => ( <li key={index}>{rec}</li> ))} </ul> </div> );}
๐ค useEffect hook can only be run when the page first loads?
True
False
If you run the above code and try typing some letters, you will see the recommendations list automatically gets updated as you add/remove new characters in the search box. This is because when you update the input box, the value of searchText
is updated through the onChange
handler, which triggers the useEffect
, which updates the recommendations
list, which updates the HTML view.
You can also similarly create side effects which are dependent on multiple state variables, not just one. If any of the dependent variables change, the side effect is run. You do this by just adding more state variables to the dependency array.
useEffect(() => { // Some code}, [stateVar1, stateVar2, stateVar3, andSoOn]);
๐ค useEffect can only be dependent on one value?
True
False
useRef
useRef
is another React hook that is somewhat commonly used. It is quite similar useState
on the surface but has some subtle differences that are actually quite important which makes this React Hook important to learn.
useRef
variables are created as follows:
function Component() { const myValue = useRef();
function updateMyValue(newValue) { myValue.current = newValue; }
function printMyValue() { console.log(myValue.current); }}
Check out my next article for continuation.