React Essentials
My React Notes
React combines components for reusable UI and hooks for managing state and lifecycle. This article covers key hooks like useState
and useEffect
with examples.
What is a Component?
Components in React is a JavaScript function that returns an HTML markup thereby allowing you to create reusable web templates/code. It is one of the key foundations in React and they always start with an uppercase letter to distinguish from vanilla HTML tags.
function SomeComponent(){
return (
<h1>This is a header</h1>
)
}
export default function Main(){
return(
<SomeComponent/>
)
}
Important stuff to know in React:
- JSX: It is an extension of HTML for Javascript. It is a convenient tool to have HTML tags inside a JavaScript file.
- Babel: A transcompiler to make the new stuff in ECMAScript 2015+ or ES6 (some JavaScript version) to be backwards compatible to ES5. Since Babel can also understand JSX, it is used by React to transcompile its components. Ex:
// Babel Input: ES2015 arrow function
[1, 2, 3].map(n => n + 1);
// Babel Output: ES5 equivalent
[1, 2, 3].map(function(n) {
return n + 1;
});
- Props: It is short for "properties". They are static parameters of componenets (they are seen attributes in HTML tags). They are usually made for rendering purposes.
Ex:
export default function Main(props) {
return (
<section>
<p>
We serve the most {props.adjective}{" "}
food around.
</p>
</section>
);
}
function App() {
return (
<div>
<Main adjective="amazing" />
</div>
);
}
- State: It is a dynamic property (unlike regular static properties). It helps a component "remember" information and are used to keep track of something. States are not functions, they are just snapshots of data that changes with every render.
What is a Hook?
Hooks in React are a function and are used to handle states for components. It is a convention for hooks to start with the word "use".
There are different types of hooks. Some are built-in and some are custom made.
Understanding useState(), useEffect(), useReducer(), and useRef() Hooks
useState()
useState
is a hook that adds a state to a component.
Syntax:
const [state, setState] = useState("state value");
useState
returns an array of two values.
- The value of the state.
- The
set
function that lets you change the value of that state.
It's like a compact getter and setter method in Java or C#.
Usage:
useState()
is best suited with user events handler like onClick
, onChange
, onSelect
or any other HTML DOM events.
import {useState} from "react";
function App(){
const [name, setName] = useState("John");
return(
<div>
<h1>Hi! my name is {name}.</h1>
<button onClick={() => setName("Peter")}>Change my name to Peter</button>
</div>
);
}
Note however that the following is the WRONG way of using useState()
. setEmotion()
will change the value only after the rendering is done (or after the return statement):
import { useState } from "react";
function App(){
//We create a state like so
const [emotion, setEmotion] = useState("Sad");
console.log(emotion.value); //emotion is "sad"
setEmotion("Happy");
//Sad does not become "Happy". But this is the wrong way of using setEmotion().
console.log(emotion.value); //Still "Sad"
return (
//It will not print anything. The console will show up with errors.
<h1>I am {emotion}</h1>
);
}
Will lead to:
Too many re-renders. React limits the number of renders to prevent an infinite loop.
Since setEmotion()
is called for every rendering on loop, the browser will then complain that it is "re-rendering" too much. From the browser's point of view, this is what it looks like:
function render(){
render();
}
render();
Because of this, we need a user event handler to prevent an infinite recursion.
setEmotion()
is supposed to be called only once every time the button is clicked.
useReducer()
useReducer()
is like useState()
. But unlike useState()
, useReducer()
accepts static logic (represented as a function) as its parameter.
const [state, function] = useReducer(reducer, initialState)
Unlike having state logics being spread out throughout the code with useState()
, useReducer()
helps organize different state logic into its respective methods.
Usage:
function reducer(state, action) {
return { condition: !state.condition};
}
function App(){
const [checked, toggleChecked] = useReducer(reducer, { condition: false });
return (
<div>
<input
type="checkbox"
checked={checked.condition}
onChange={toggleChecked}
/>
<label>{checked.condition? "checked" : "not checked"}</label>
</div>
);
}
Or you can use an arrow function to make the code A LOT more legible.
function App(){
const [checked, toggleChecked] = useReducer((checked) => !checked, false );
return (
<div>
<input
type="checkbox"
checked={checked}
onChange={toggleChecked}
/>
<label>{checked? "checked" : "not checked"}</label>
</div>
);
}
useEffect()
useEffect()
is a hook that accepts a function and an optional list.
With useState()
, you (re-)initialize a component with a state. With useEffect()
, you can perform side effects like directly fetching or updating the state of a component. It is made to output unpredictable results for the user.
useEffect()
is made to address the common misuse of useState()
. However, its intended use is mainly for dealing data with third-party tools like backend servers, browser APIs, and timing functions like setTimeout()
and setInterval()
.
In short, if you want an expected outcome, use useState()
. If you want the webpage to interact with the outside world or expect unexpected results, use useEffect()
.
If your code does not involve (a)synchronization, you do not need useEffect()
. More here.
There are three possible usage scenarios:
- Without a dependency (2nd param). BAD USAGE
setEffect( () => {
//1st param: code that runs after *every* render
});
- With an empty list
setEffect( () => {
//1st param: code that runs only on the first render
}, []);
- With a list
setEffect( () => {
//1st param: code that runs on the first render then after any dependency value change
},
//2nd param: A dependency array of states specified. If its values change, the code in the 1st param above will run again.
[...,...,...]);
Usage:
To demonstrate how useEffect()
looks like without a dependency, We use a setTimeout()
method:
useEffect(() => {
setTimeout(() => {
setCount((count) => count + 1);
}, 1000);
});
return <h1>I've rendered {count} times!</h1>;
The rendering does not stop as the dependency does not exist. So we have to add an empty array as our second parameter.
useEffect(() => {
setTimeout(() => {
setCount((count) => count + 1);
}, 1000);
}, []);
return <h1>I've rendered {count} times!</h1>;
Now it only renders once.
useRef()
All of the hooks mentioned above will require re-rendering when then their values are changed. useRef()
however, is the exception.
"ref" is short for reference. It lets you "reference"/remember a value without the need of re-rendering.
const ref = useRef(initialValue)
Note however that states and refs are two different things. useRef()
does not rely on states as states trigger re-rendering when they are changed. A ref is just a plain JavaScript that can store a value that is used for later use.
If we pass 0 to useRef()
,
const ref = useRef(0);
useRef()
returns an object like so (the browser will "see" this):
{
current: 0 //the value passed to useRef
}
"curent" is just an attribute of ref (ref.current
).
Usage
Here, we hold the number of times the user clicked on a button. Unlike useState()
, it does not trigger a re-render when the value is changed.
import { useRef } from 'react';
export default function Counter() {
let ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
alert('You clicked '+ref.current+' times!');
}
return (
<div>
<h1>You clicked me {ref.current} time(s)!</h1>
<button onClick={handleClick}>
Click me!
</button>
</div>
);
}
Notice how the value of ref.current
in the return statement is not updated. Refs do not re-render its components when they are re-rendered. They only serve as a storage space for later use.
This table from the manual is excellent to distinguish between refs and states apart.
Fetching Data with Hooks
There is a link to output a user's data on Github. Let's the take the following example:
https://api.github.com/users/Garenium
This shows a user's Github data respresented as a JSON object. In order to fetch this with React, we use useEffect()
like so:
import "./App.css";
import { useState, useEffect } from "react";
import { useReducer } from "react";
function App() {
const [data,setData] = useState(null);
useEffect(() => {
fetch(
`https://api.github.com/users/Garenium`
).then((response) => response.json())
.then(setData);
}, []);
if(data) return <pre>{JSON.stringify(data,null,2)}</pre>
return (
<h1>Data</h1>
);
}
export default App;
The problem with this code is that it cannot handle three different states:
- When the page is loading
- When the page is finished loading
- When there is a fetch problem
Notice that when the webpage was loading, it was showing "Data" in a blink of an eye instead of the actual JSON object. This is because it is the asynchronous nature of the useffect
hook. If we were to encounter a Github username that didn't exist, we would see this:
It is recommended to create three different states like so:
import "./App.css";
import { useState, useEffect } from "react";
//Create a separate component for extract Github user data after useEffect is
//done
function GithubUser({ name, location, avatar }) {
return (
<div>
<h1>{name}</h1>
<p>{location}</p>
<img src={avatar} height={150} alt={name} />
</div>
);
}
function App() {
//The three states:
const [data, setData] = useState(null); //Data to load fetch
const [error, setError] = useState(null); //setError when fetching fails
const [loading, setLoading] = useState(false); //setLoading before data is set
useEffect(() => {
setLoading(true);
fetch(
`https://api.github.com/users/moonhighway`
)
.then((response) => response.json())
.then(setData)
.then(() => setLoad(false))
.catch(setError);
}, []);
if(loading) return <h1>Loading...</h1> //Show this when the state is loading
if(error) return <pre>{JSON.stringify(error)}</pre> //When fetching fails. stringify error
if(!data) return null; //return null when there's no data
return (
<GithubUser
name={data.name}
location={data.location}
avatar={data.avatar_url}
/>
);
}
export default App;