React
Welcome to React!
React is the most popular frontend library/framework in the world right now. Every company (except google) yang punya website most likely pake React. It’s a really good thing because if you are familiar with React, you’ll easily be valued in a lot of companies’ eyes, but it’s also a “bad” thing because with the amount of people using React, you’ll have a harder competition with others. But of course, for members of NCS mah ini harusnya jadi motivasi. Ok let’s get into it yuh.
Prerequisites
-
Required:
- JS
- JS array methods: map, filter, etc.
- A bit of HTML
- A bit of CSS
- A package manager (npm, yarn, or pnpm)
- Desctructuring of arrays and objects
-
VERY helpful to know:
- Arrow functions
- Anonymous arrow functions
- Async/await
???
So back then before web frameworks became popular, the way we build website and webapps is by utilizing HTML + CSS + JS. React is no different in the sense that it also uses HTML + CSS + JS, but the thing that makes it different is where you put the HTML, CSS, and JS. Let’s look at a small example.
Vanilla:
<script>
const name = "Groot";
const nameNode = document.getElementById("name");
nameNode.innerHTML = name;
const items = ["i", "am", "groot"];
const listNode = document.getElementById("list");
for (let i = 0; i < items.length; ++i) {
const text = items[i];
const childNode = document.createElement("li");
childNode.innerHTML = text;
listNode.appendChild(childNode);
}
</script>
<body>
<h1 id="name"></h1>
<ul id="list"></ul>
</body>
React:
const name = "Groot";
const items = ["i", "am", "groot"];
return (
<body>
<h1>{name}</h1>
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
</body>
);
So as you can see, React’s way is more declarative and readable. This syntax is called JSX, which is basically HTML but you can put JS in it.
Some people say “learn the vanilla way before learning React”, but actually, I learned React way before I learned vanilla. I learned the vanilla way around 1 year after using React, and I turned out just fine. I’ll be honest, the knowledge of using the vanilla way is SUPER helpful sometimes because 2% of the time, you will have to use some form of “vanilla way”.
Another thing to note is something you might / might not be thinking already — we don’t always need React. React is useful when you want an interactive/dynamic site aka apps (chess, tic tac toe, todo list, calculator, etc), but it also comes with extra JS which can sometimes be bad, and which is why frameworks like Astro and Eleventy exist. They ship no JS by default and is useful if you want a static site like blogs, portfolio, etc. This website is built using Astro :) hehe
BUT WE’RE LEARNING REACT SO LET’S FOCUS ON THAT RIGHT NOW
Starting a plain React project
There are a couple tools for this:
Create React App is the OG way of doing it, but some guys hate how slow it is, so they made Vite. I think since late 2021 - early 2022, Vite has takenover as the “optimal way” to start a plain React project. So let’s use that :)
If you want npm
npm create vite@latest
If you want yarn
yarn create vite
If you want pnpm
pnpm create vite
and then:
- Type your project name
- Choose React
- Choose JavaScript / TypeScript
or a shortcut:
# npm
npm create vite@latest PROJECT_NAME -- --template react
# yarn
yarn create vite PROJECTNAME --template react
# pnpm
pnpm create vite PROJECT_NAME --template react
replace PROJECT_NAME with the name of your project.
Components
If we’re using React, we must think of websites as a bunch of components. Components itu a reusable piece of code that renders a HTML element. It is basically an abstraction, which is a lot like functions. Let’s revisit the first example I gave and turn it into a bunch of components.
Before:
const name = "Groot";
const items = ["i", "am", "groot"];
return (
<body>
<h1>{name}</h1>
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</body>
);
After:
const BigName = () => {
const name = "Groot";
return <h1>{name}</h1>;
};
const ListOfItems = () => {
const items = ["i", "am", "groot"];
return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
const items = ["i", "am", "groot"];
};
const App = () => {
return (
<body>
<BigName />
<ListOfItems />
</body>
);
};
And usually, we will break this up into several files, so it will end up looking like this:
import BigName from "./BigName";
import ListOfItems from "./ListOfItems";
const App = () => {
return (
<body>
<BigName />
<ListOfItems />
</body>
);
};
The code above is a lot more readable. Someone who is not familiar with the codebase can guess/know what App will display just based on reading it. This is also one of React’s selling point. You write code as if you’re speaking english (decralative). Well, “writing readable code” is one of the traits of a good programmer. So if some bad programmer uses React, it will be just as unreadable as using vanilla (if not worse). What React provides to us is a way for us to write readable shit.
One more thing to note is that a component must only return ONE element/thing.
// this is wrong!
const Component = () => {
return (
<div>Two</div>
<div>Things</div>
)
};
// this is correct!
const Component = () => {
return (
<div>One Thing</div>
)
};
But what if you need to return more than one element? 🤔
We use this thing called Fragments. They look like this: <> </>
So now, we can do this:
// this is now correct!
const Component = () => {
return (
<>
<div>Two</div>
<div>Things</div>
</>
);
};
An example of a piece of code using components and fragments from the real world that I can show you is a code from HereToShare’s website (https://heretoshare.id/).
import Head from "next/head";
import About from "@/modules/About";
import Navbar from "@/modules/Navbar";
import Hero from "@/modules/Hero";
import Projects from "@/modules/projects/Projects";
import Achievements from "@/modules/Achievements";
import FAQ from "@/modules/faq/Faq";
import Layout from "@/layouts/DefaultLayout";
import { useRouter } from "next/router";
import { useEffect } from "react";
import Reviews from "@/modules/Reviews";
const HomePage = () => {
return (
<>
<Head>
<title>Here to Share</title>
</Head>
<Navbar />
<Layout>
<Hero />
<About />
<Achievements />
<Projects />
<Reviews />
<FAQ />
</Layout>
</>
);
};
export default HomePage;
Another thing you might / might not notice is how we name these components. We HAVE TO use PascalCase.
And just in case you don’t know what that is, here’s a list of the cases we use in dev:
- snake case:
some_function_name - camel case:
someFunctionName - kebab case:
some-function-name - pascal case:
SomeFunctionName - train case:
Some-Function-Name - macro case:
SOME_FUNCTION_NAME
Hooks
This is what makes React… React. In modern React we use what we call Functional Components. You define a function that return one HTML element (like the examples above), and you’re done. Back then we always use Class Components. In Class Components, we can explicitly see the components lifecycle (when it’s about to mount, when it first mounts, when it’s about to unmount, etc). But we don’t see that anymore since hooks came out.
There are 15 hooks in total (i think) and you can even make your own later, but you can start (and survive a junior dev role) with just 2 hooks: useState and useEffect
useState
A component will use this hook to “remember” information.
Some examples are:
- The number displayed on a stopwatch
- The email some user types in a login form
- The number of times you click a button
This hook has 1 parameter which will be the default value, and returns an array of 2 items: a state variable and a state setter function.
We can then destructure this array like this:
const [state, setState] = useState(0);
So in the code above, we first set the default value to 0 and assign this value to state.
If we want to change the value of state, we don’t do state = 5 or state = state + 1, but we use the setter function like this:
setState(5); // this is saying `state = 5`
setState(state + 1); // this is saying `state = state + 1`
Although you can name those 2 things whatever you want like this:
const [bob, chris] = useState("huh");
It is better to name them like this:
const [number, setNumber] = useState(0);
const [name, setName] = useState("");
const [country, setCountry] = useState("Indonesia");
const [person, setPerson] = useState({ name: "", age: 0 });
Let’s make a simple counter that has a + button and a - button.
import { useState } from "react";
const Counter = () => {
const [counter, setCounter] = useState(0);
return (
<div>
<div>{counter}</div>
<button>+</button>
<button>-</button>
</div>
);
};
We can then add functionalities to the buttons like this:
import { useState } from "react";
const Counter = () => {
const [counter, setCounter] = useState(0);
const handleAdd = () => setCounter(counter + 1);
const handleSubtract = () => setCounter(counter - 1);
return (
<div>
<div>{counter}</div>
<button onClick={handleAdd}>+</button>
<button onClick={handleSubtract}>-</button>
</div>
);
};
We can also add a reset button to turn the counter to 0.
import { useState } from "react";
const Counter = () => {
const [counter, setCounter] = useState(0);
const handleAdd = () => setCounter(counter + 1);
const handleSubtract = () => setCounter(counter - 1);
const handleReset = () => setCounter(0);
return (
<div>
<div>{counter}</div>
<button onClick={handleAdd}>+</button>
<button onClick={handleSubtract}>-</button>
<button onClick={handleReset}>reset</button>
</div>
);
};
And then with a bit of CSS, it will look like this:
Accessing previous states through useState
Let’s say that we need to add a button that adds 5 to the counter. As a programmer, I would want to re-use code if possible, so I do this:
import { useState } from "react";
const Counter = () => {
const [counter, setCounter] = useState(0);
const handleAdd = () => setCounter(counter + 1);
const handleSubtract = () => setCounter(counter - 1);
const handleReset = () => setCounter(0);
const handleAdd5 = () => {
for(int i = 0; i<5; ++i) {
handleAdd();
}
};
return (
<div>
<div>{counter}</div>
<button onClick={handleAdd5}>+5</button>
<button onClick={handleAdd}>+</button>
<button onClick={handleSubtract}>-</button>
<button onClick={handleReset}>reset</button>
</div>
);
}
This won’t actually work. The reasons are a bit technical, so if you don’t wanna read technical stuff today, please skip to the next section.
The reason behind this is how the setter function (in this case it’s setCounter) and React in general works.
So if you see something changes, React is doing a re-render of the part at
which you see this change. A re-render is basically unmounting (“deleting”) and
then remounting. Seeing the number increasing whenever you press + is actually
React “deleting” that element and then “putting in” a new one.
And the way the setter function works is it will replace the state with whatever
the thing inside its parentheses is returning and updates it before it
re-renders. So what the handleAdd5 function is doing now is something like this:
let counter = 0;
let newCounter = 0;
for (let i = 0; i < 5; ++i) {
newCounter = counter + 1; // does not update counter
// this means that newCounter will always be 0 + 1 which is 1
}
counter = newCounter; // update counter, then re-render
This is what the counter will look like with that code
So how we fix this is by passing in a function instead of just passing in a value.
// before
const handleAdd = () => setCounter(counter + 1);
// after
const handleAdd = () => setCounter((c) => c + 1);
In the // after code above, the variable in the function’s parameter (c) is the
previous state.
This is actually the “best practice” when you need to change a state based on its previous state. So now the code should look like this:
import { useState } from "react";
const Counter = () => {
const [counter, setCounter] = useState(0);
const handleAdd = () => setCounter((c) => c + 1);
const handleSubtract = () => setCounter((c) => c - 1);
const handleReset = () => setCounter(0);
const handleAdd5 = () => {
for (let i = 0; i < 5; ++i) {
handleAdd();
}
};
return (
<div>
<div>{counter}</div>
<button onClick={handleAdd}>+5</button>
<button onClick={handleAdd}>+</button>
<button onClick={handleSubtract}>-</button>
<button onClick={handleReset}>reset</button>
</div>
);
};
Note that I can also do
const handleAdd5 = () => setCounter((c) => c + 5);
I just did the loop as an example :)
One more thing you should notice is that I didn’t change the handleReset
function. The reason is that resetting the counter to 0 doesn’t depend on its
previous states. It doesn’t matter if the counter is at 69, 420, -1, or
whatever. We always reset to 0. So in the case of the handleReset function, we
can just put setCounter(0).
Another thing you should pay attention is the naming of the variables in the setter function. We usually just use the first letter of the state variable. I’ll give you a few examples.
const [age, setAge] = useState(0);
const handleAdd = () => setAge((a) => a + 1);
const [car, setCar] = useState({brand: "BMW", year: 2000});
const handleAddYear = () => setCar((c) => {...c, year: c.year + 1})
const [height, setHeight] = useState(170);
const handleGrow = () => setHeight((h) => h + 20);
useEffect
useState is used for “internal” changes. So stuff like “press this button to
increment the counter”, “type something to make another thing appear”, etc. All
these are something we define and then interact with. On the other hand,
useEffect is used to synchronize with external systems like your friend’s API,
google analytics, etc.
useEffect takes 2 parameters:
- A function that it will run (required)
- An array of dependencies aka variables that it will watch (optional).
const NameForm = () => {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
useEffect(() => {
console.log("run effect!");
});
return (
<div>
<input
value={firstName}
placeholder="Put your first name here"
onChange={(e) => {
setFirstName(e.target.value);
}}
/>
<input
value={lastName}
placeholder="Put your name here"
onChange={(e) => {
setLastName(e.target.value);
}}
/>
</div>
);
};
So useEffect will run the function you gave it one time when the component
renders/mounts for the first time, and it will re-run the function when the
component re-renders.
What if you don’t want to re-run the function everytime the component re-renders? This is when you will put the array of dependencies.
So if you do this:
useEffect(() => {
console.log("run effect!");
}, []); // empty array
the function will only run once (which is when the component renders for the first time). Some usecases for this is when you want to track how many times someone has visited your website.
And if you do this:
useEffect(() => {
console.log("run effect!");
}, [firstName]);
It will run the function when the component renders for the first time, and also every time the variable firstName changes.
So you can see this for yourself, here’s the NameForm component we saw earlier:
const NameForm = () => {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
useEffect(() => {
console.log(`you typed: ${firstName}`);
}, [firstName]);
return (
<div>
<input
value={firstName}
placeholder="Put your first name here..."
onChange={(e) => {
setFirstName(e.target.value);
}}
/>
<input
value={lastName}
placeholder="Put your last name here..."
onChange={(e) => {
setLastName(e.target.value);
}}
/>
</div>
);
};
The effect will log “you typed: …” in your console. So open up your browser console and play around with this form:
Notice when you type in the lastName input, it does not run the effect
because lastName is not in the array of variables that useEffect will watch.
Data fetching with useEffect
Our previous usecase of an effect is mostly for debugging. Now, we will cover a more common usecase for effects. We’ll be fetching some advice quotes from Advice Slip’s API.
You can click the link and take a look at the object it will return. The API will return an object with this form:
{
"slip": {
"id": number, // random integer
"advice": string // random advice
}
}
The code will look like this:
const AdviceCard = () => {
const [id, setId] = useState();
const [advice, setAdvice] = useState();
const fetchAdvice = async () => {
const res = await fetch("https://api.adviceslip.com/advice"); // fetch
const data = await res.json(); // turn the response into a JS object
setId(data.slip.id); // access object.slip.id and assign to `id`
setAdvice(data.slip.advice); // same thing but to `advice`
};
useEffect(() => {
fetchAdvice();
}, []);
return (
<div>
<div>Advice #{id}</div>
<div>{advice}</div>
</div>
);
};
So since we only want to run the effect one time (which is when it first renders), we keep the dependency array empty.
The result:
You can refresh this page to see the advice change. It changes because the API
returns a different advice every time we call it, and when you refresh the page,
the component unmounts and remounts “for the first time” which causes the
useEffect to run.
Conditional rendering
As the name implies, this is a technique to render components only when we want
it to. There are a lot of usecases for this technique and we will look at one of them now to improve the UX of the AdviceCard component we saw earlier.
So when you refresh the page and see the advice card, it will first show nothing except “Advice #”, and then once we get the data back, the content suddenly pops up out of nowhere. A good way to improve the UX / user experience of this is to conditionally render the advice id and content. We only want to show them the id and content once we get the data.
const AdviceCard = () => {
const [id, setId] = useState(); // default value is undefined
const [advice, setAdvice] = useState(); // default value is undefined
const fetchAdvice = async () => {
const res = await fetch("https://api.adviceslip.com/advice"); // fetch
const data = await res.json(); // turn the response into a JS object
setId(data.slip.id); // access object.slip.id and assign to `id`
setAdvice(data.slip.advice); // same thing but to `advice`
};
useEffect(() => {
fetchAdvice();
}, []);
// check if id and advice exist
// render the thing we want to show if id and advice exist
// else render null (nothing)
return (
<div>
{id && advice ? (
<>
<div>Advice #{id}</div>
<div>{advice}</div>
</>
) : null}
</div>
);
};
We used a ternary to see if id and advice exist.
If you’re confused by the syntax, it’s basically saying this:
// we set id's and advice's default values to undefined
if (id && advice) {
return (
<>
<div>Advice #{id}</div>
<div>{advice}</div>
</>
);
} else {
return null;
}
So now it will look like this:
This is better because we don’t show anything incomplete to the user, but it’s still… weird. An even better way to improve this is to implement a loading state. An easy way to implement a loading state is to use another state variable.
Like this:
const AdviceCard = () => {
const [id, setId] = useState();
const [advice, setAdvice] = useState();
const [isLoading, setIsLoading] = useState(true); // default value is true
const fetchAdvice = async () => {
setIsLoading(true); // set loading state to true when we're about to fetch
const res = await fetch("https://api.adviceslip.com/advice");
const data = await res.json();
setId(data.slip.id);
setAdvice(data.slip.advice);
setIsLoading(false); // set loading state to false when we're done setting up
};
useEffect(() => {
fetchAdvice();
}, []);
// check if isLoading is true
// if isLoading is true render Loader component (code for this is not shown)
// else render the thing we want to show
return (
<div>
{isLoading ? (
<Loader /> // a component you made that will show a loading state
) : (
<>
<div>Advice #{id}</div>
<div>{advice}</div>
</>
)}
</div>
);
};
So now the advice card will look like this:
In a perfect world, this would be good enough because in a perfect world, we would be expecting no errors and the data will always come through. In reality, that’s not always the case. Even big companies like WhatsApp can sometimes be unreliable. So we can actually improve the our advice card component even further by implementing an error state.
const AdviceCard = () => {
const [id, setId] = useState();
const [advice, setAdvice] = useState();
const [isLoading, setIsLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState("");
const fetchAdvice = async () => {
// try catch block
try {
// try to do this
setIsLoading(true);
const res = await fetch("https://api.adviceslip.com/advice");
const data = await res.json();
setId(data.slip.id);
setAdvice(data.slip.advice);
setIsLoading(false);
} catch (err) {
// if the `try` somehow fails,
// set the error message
setErrorMessage(err);
}
};
useEffect(() => {
fetchAdvice();
}, []);
// if errorMessage is exists, render this
if (errorMessage) return <div>{error}</div>;
// else, render this
return (
<div>
{isLoading ? (
<Loader />
) : (
<>
<div>Advice #{id}</div>
<div>{advice}</div>
</>
)}
</div>
);
};