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

???

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:

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:


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:

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:

0

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

0

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>
  );
};
0

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:

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:

Advice #

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>
  );
};