Advanced
Properties of Hooks
All the hooks
have the following properties:
- They start with the word
use
- The component where they are created must be named in uppercase
- They cannot be invoked inside a function/component body.
- You cannot call hooks conditionally
useState
Error
In the next piece of code we show how, if we change the value of a variable in React
, it does not change in our web app because it is not re-rendered:
import React from "react";
const ErrorExample = () => {
let title = "random title";
const handleClick = () => {
title = "hello people";
console.log(title);
};
return (
<React.Fragment>
<h2>{title}</h2>
<button type="button" onClick={handleClick}>
change title
</button>
</React.Fragment>
);
};
export default ErrorExample;
That is why we will need to use the hook useState
, so we change handle state changes.
import React, { useState } from "react";
const UseStateBasics = () => {
const [text, setText] = useState("random title");
const handleClick = () => {
if (text === "random title") {
setText("hello world");
} else {
setText("random title");
}
};
return (
<React.Fragment>
<h1>{text}</h1>
<button type="button" onClick={handleClick}>
change title
</button>
</React.Fragment>
);
};
export default UseStateBasics;
When we invoke useState
we have to pass as an argument the initial value of the state variable. useState
is a function that returns an array:
- The first element: the state variable
- The second element: the handler that controls the value of the state value
When using useState
with objects, whenever you update one property of the object, you have to pass the object to the handler (with the spread operator), and then override the property you want to update:
import React, { useState } from "react";
const UseStateObject = () => {
// Object
const [person, setPerson] = useState({
name: "peter",
age: 24,
message: "random message",
});
const changeMessage = () => {
// Pass the person object with the spread operator
// and override the message property
setPerson({ ...person, message: "hello world" });
};
return (
<>
<h3>{person.name}</h3>
<h3>{person.age}</h3>
<h4>{person.message}</h4>
<button className="btn" onClick={changeMessage}>
change message
</button>
</>
);
};
export default UseStateObject;
Asynchronous functions
If we want to update a value asynchronally, and fetch the value of the state variable when the change happens, and not when the function is defined, then:
import React, { useState } from "react";
const UseStateCounter = () => {
const [value, setValue] = useState(0);
const reset = () => {
setValue(0);
};
const complexIncrease = () => {
setTimeout(() => {
// value is the value of the state variable when the timeout is defined
// if you call it multiple times consecutively you get the same value, because they all get value = 0
// setValue(value + 1);
// prevState is the value of the state variable when the timeout finished
// if you call it multiple times consecutively you get different values, because value has already been updated
// by another setTimeout.
// if you call it multiple times
setValue((prevState) => {
return prevState + 1;
});
}, 2000);
};
return (
<>
<section style={{ margin: "4rem 0" }}>
<h2>more complex counter</h2>
<h1>{value}</h1>
<button className="btn" onClick={complexIncrease}>
increase later
</button>
</section>
</>
);
};
export default UseStateCounter;
useEffect
Dependencies
The useEffect
definition allows you to pass an array of dependencies:
- If it is specified as
[]
:useEffect
will only be triggered in the first render - If it is an array of state variables: it will be triggered every time the state variable is updated.
import React, { useState, useEffect } from 'react';
const UseEffectBasics = () => {
const [value, setValue] = useState(0);
// Only trigger on first render
// useEffect(() => {
// document.title = `New Messages(${value})`;
// }, []);
// Call whenever value is updated
useEffect(() => {
document.title = `New Messages(${value})`;
}, [value]);
return (
<>
<h1>{value}</h1>
<button className='btn'}>
click me
</button>
</>
);
};
export default UseEffectBasics;
Clean up Function
useEffect
lets us define a function that is invoked once we exit the function:
import React, { useState, useEffect } from "react";
const UseEffectCleanup = () => {
const [size, setSize] = useState(window.innerWidth);
const checkSize = () => {
setSize(window.innerWidth);
};
useEffect(() => {
console.log("useEffect");
window.addEventListener("resize", checkSize);
// Clean up function
return () => {
console.log("cleanup");
window.removeEventListener("resize", checkSize);
};
}, []);
console.log("render");
return (
<>
<h1>window</h1>
<h2>{size} PX</h2>
</>
);
};
export default UseEffectCleanup;
Fetch Data
Up next we will show how to get data using useEffect
.
Note, if we do not specify the restriction of only triggering on the first render:
useEffect
calls getUsersgetUsers
updates the state, and so the component re-renders- Because there is a re-render,
useEffect
is called again Thus, we end in an infinite loop
import React, { useState, useEffect } from "react";
const url = "https://api.github.com/users";
const UseEffectFetchData = () => {
const [users, setUsers] = useState([]);
const getUsers = async () => {
const response = await fetch(url);
const users = await response.json();
setUsers(users);
};
useEffect(() => {
getUsers();
// Specify [] so we only run useEffect on the first render.
}, []);
return (
<>
<h3>github users</h3>
<ul className="users">
{users.map((user) => {
const { id, login, avatar_url, html_url } = user;
return (
<li key={id}>
<img src={avatar_url} alt={login} />
<div>
<h4>{login}</h4>
<a href={html_url}>profile</a>
</div>
</li>
);
})}
</ul>
</>
);
};
export default UseEffectFetchData;
Conditional Rendering
Short Circuit Evaluation
Now, let’s see an example of Short Circuit Evaluation
in action:
import React, { useState } from "react";
const ShortCircuit = () => {
const [text, setText] = useState("");
const [isError, setIsError] = useState(false);
// If text is falsy, then return 'hello world'
// else return text
// const firstValue = text || 'hello world';
// If text is true, then return 'hello world'
// else return text
// const secondValue = text && 'hello world';
return (
<>
{/**If text is false, return h1 with 'john doe value'**/}
<h1>{text || "john doe"}</h1>
{/**If text is true, return h1 with 'john doe value'**/}
{text && <h1>'john doe'</h1>}
</>
);
};
export default ShortCircuit;
Ternary operators
We can also use ternary operators
to render conditionally in React
.
import React, { useState } from "react";
const ShortCircuit = () => {
const [isError, setIsError] = useState(false);
return (
<>
<button className="btn" onClick={() => setIsError(!isError)}>
toggle error
</button>
{/*Check the value of isError, if is error is true, return the first value after the ?
else return the second value*/}
{isError ? (
<p>there is an error...</p>
) : (
<div>
<h2>there is no error</h2>
</div>
)}
</>
);
};
export default ShortCircuit;
Controlled Inputs
Multiple inputs
How can we define an event handler for the OnChange
event that is generic, instead of defining one for each input? To showcase this scenario, we will use the same code as before, but with two new inputs. All of the inputs have the same OnChange
event handler.
import React, { useState } from "react";
const ControlledInputs = () => {
// Create a new state variable person, that holds the properties of the person we are currently creating
const [person, setPerson] = useState({ firstName: "", email: "", age: "" });
// Array of people we have already created
const [people, setPeople] = useState([]);
// Generic event handler
const handleChange = (e) => {
// Obtain the name of the input/state variable
const name = e.target.name;
// Obtain the new value for the input
const value = e.target.value;
// Update the value of the property for the current person
setPerson({ ...person, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
if (person.firstName && person.email && person.age) {
const newPerson = { ...person, id: new Date().getTime().toString() };
setPeople([...people, newPerson]);
setPerson({ firstName: "", email: "", age: "" });
}
};
return (
<>
<article className="form">
<form>
<div className="form-control">
<label htmlFor="firstName">Name : </label>
<input
type="text"
id="firstName"
name="firstName"
// Access the firstName of the person object
value={person.firstName}
// Generic event handler
onChange={handleChange}
/>
</div>
<div className="form-control">
<label htmlFor="email">Email : </label>
<input
type="email"
id="email"
name="email"
// Access the email of the person object
value={person.email}
// Generic event handler
onChange={handleChange}
/>
</div>
<div className="form-control">
<label htmlFor="age">Age : </label>
<input
type="number"
id="age"
name="age"
// Access the age of the person object
value={person.age}
// Generic event handler
onChange={handleChange}
/>
</div>
<button type="submit" className="btn" onClick={handleSubmit}>
add person
</button>
</form>
</article>
<article>
{people.map((person) => {
const { id, firstName, email, age } = person;
return (
<div key={id} className="item">
<h4>{firstName}</h4>
<p>{email}</p>
<p>{age}</p>
</div>
);
})}
</article>
</>
);
};
export default ControlledInputs;
useRef
useRef
returns a mutable ref object whose .current
property is initialized to the passed argument. Some properties:
- Preserves the value of the object
- Does not trigger re-render
- Assigned to
DOM nodes/elements
import React, { useEffect, useRef } from "react";
const UseRefBasics = () => {
// Create the container
const refContainer = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
// Print the value inside the input
console.log(refContainer.current.value);
};
useEffect(() => {
// Focus on the input element whenever we render the application
refContainer.current.focus();
});
return (
<>
<form className="form" onSubmit={handleSubmit}>
<div>
{/**The refContainer points to the input element**/}
<input type="text" ref={refContainer} />
</div>
<button type="submit">submit</button>
</form>
</>
);
};
export default UseRefBasics;
useReducer
An alternative to useState
. Accepts a reducer of type (state, action) => newState
, and returns the current state paired with a dispatch method.
useReducer
is usually preferable to useState
when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer
also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
For example:
import React, { useState, useReducer } from "react";
// Components
import Modal from "./Modal";
// Data
import { data } from "../../../data";
// Reducer dispatch function
import { reducer } from "./reducer";
// Initial state for the reducer
const defaultState = {
people: [],
isModalOpen: false,
modalContent: "",
};
const Index = () => {
// Define state variables
const [name, setName] = useState("");
// Define reducer: (dispatch fuction, initial state)
const [state, dispatch] = useReducer(reducer, defaultState);
const handleSubmit = (e) => {
// Avoid the re-rendering caused by the submit event
e.preventDefault();
if (name) {
const newItem = { id: new Date().getTime().toString(), name };
// Call reducer to update state
dispatch({ type: "ADD_ITEM", payload: newItem });
setName("");
} else {
// Call reducer to update state
dispatch({ type: "NO_VALUE" });
}
};
const closeModal = () => {
// Call reducer to update state
dispatch({ type: "CLOSE_MODAL" });
};
return (
<>
{/**Render Modal component conditionally **/}
{state.isModalOpen && (
<Modal closeModal={closeModal} modalContent={state.modalContent} />
)}
{/** Form to add a new person to the reducer state variable **/}
<form onSubmit={handleSubmit} className="form">
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<button type="submit">add </button>
</form>
{/** Show the people stored in the reducer state variable **/}
{state.people.map((person) => {
return (
<div key={person.id} className="item">
<h4>{person.name}</h4>
<button
onClick={() =>
// Call reducer to update state
dispatch({ type: "REMOVE_ITEM", payload: person.id })
}
>
remove
</button>
</div>
);
})}
</>
);
};
export default Index;
Now, let’s see the reducer
function:
/** Reducer function **/
export const reducer = (state, action) => {
// Define logic for each type of action
if (action.type === "ADD_ITEM") {
// Add new person (action.payload) to existing people array (state.people)
const newPeople = [...state.people, action.payload];
return {
// Always copy the value from the previous state
...state,
// Update the people array
people: newPeople,
isModalOpen: true,
modalContent: "item added",
};
}
if (action.type === "NO_VALUE") {
// Always copy the value from the previous state
return { ...state, isModalOpen: true, modalContent: "please enter value" };
}
if (action.type === "CLOSE_MODAL") {
return { ...state, isModalOpen: false };
}
if (action.type === "REMOVE_ITEM") {
// Filter people array, by removing the person
const newPeople = state.people.filter(
(person) => person.id !== action.payload
);
// Copy the previous state (...state) and update the people the array (newPeople)
return { ...state, people: newPeople };
}
throw new Error("no matching action type");
};
Prop Drilling
Prop Drilling refers to the scenario where we have to pass props
to anidated components recursively. Next up, we show and example
import React, { useState } from "react";
// Data
import { data } from "../../../data";
// Outer component
const PropDrilling = () => {
// State passed as a prop
const [people, setPeople] = useState(data);
// Event handler passed as a prop
const removePerson = (id) => {
setPeople((people) => {
return people.filter((person) => person.id !== id);
});
};
return (
<section>
<h3>prop drilling</h3>
{/** Pass props to the list elements **/}
<List people={people} removePerson={removePerson} />
</section>
);
};
// Middle component
const List = ({ people, removePerson }) => {
return (
<>
{people.map((person) => {
{
/** Pass props to the SinglePerson elements **/
}
return (
<SinglePerson
key={person.id}
{...person}
removePerson={removePerson}
/>
);
})}
</>
);
};
// Inner component
const SinglePerson = ({ id, name, removePerson }) => {
return (
<div className="item">
<h4>{name}</h4>
<button onClick={() => removePerson(id)}>remove</button>
</div>
);
};
export default PropDrilling;
In these cases we can use the Context API
Context API
Context API
and useContext
allows us to resolve the issue of the prop drilling. The context has two components:
- The provider: works as a distributer
- The consumer
We use them as follows:
import React, { useState, useContext } from 'react';
import { data } from '../../../data';
// Create context object
const PersonContext = React.createContext();
const ContextAPI = () => {
// State saved in the context
const [people, setPeople] = useState(data);
// Event handler saved in the context
const removePerson = (id) => {
setPeople((people) => {
return people.filter((person) => person.id !== id);
});
};
return (
{/*Wrap the components in the context provider, so all the nested components
have access to the variables defined in the context object*/}
<PersonContext.Provider value={{ removePerson, people }}>
<h3>Context API / useContext</h3>
<List />
</PersonContext.Provider>
);
};
const List = () => {
// Obtain data from the context with the useContext hook
const mainData = useContext(PersonContext);
return (
<>
{mainData.people.map((person) => {
return <SinglePerson key={person.id} {...person} />;
})}
</>
);
};
const SinglePerson = ({ id, name }) => {
// Obtain data from the context with the useContext hook
const { removePerson } = useContext(PersonContext);
return (
<div className='item'>
<h4>{name}</h4>
<button onClick={() => removePerson(id)}>remove</button>
</div>
);
};
export default ContextAPI;
Custom Hooks
Customs hooks allow us to avoid duplicating code that uses hooks and essentially in different places of your code. For example, the fetching function is very common, so we create a useFetch
hook.
When you define a custom hook, that is, if you define a function outside a component that uses hooks, you will have to name it use<FunctionName>
, else you will get an error.
import React, { useState, useEffect } from "react";
// Import custom hook
import { useFetch } from "./2-useFetch";
const url = "https://course-api.com/javascript-store-products";
const Example = () => {
// Values returned by useFetch
const { loading, products } = useFetch(url);
return (
<div>
<h2>{loading ? "loading..." : "data"}</h2>
</div>
);
};
export default Example;
import { useState, useEffect, useCallback } from "react";
export const useFetch = (url) => {
// State within the hook
const [loading, setLoading] = useState(true);
const [products, setProducts] = useState([]);
// Functionality of the hook
const getProducts = useCallback(async () => {
const response = await fetch(url);
const products = await response.json();
setProducts(products);
setLoading(false);
}, [url]);
// Run whenever the url or the getProducts function changes
useEffect(() => {
getProducts();
}, [url, getProducts]);
// Values returned by the custom hook
return { loading, products };
};
Note we are using the hook useCallback
(refer to Performance Optimization), we do this because we are specifying getProducts
as a dependency for useEffect
. However getProducts
is created every time the state changes.
So when we call useEffect
, we change the state, and therefore create the function getProducts
, which triggers useEffect
, thus the state changes, and we create getProducts
, and so on and so forth.
To avoid this, we use useCallback
, which will create the function whenever any of the dependencies in the list change. So this means, now getProducts
is only created when the url
changes. This allows us to avoid the infinite loop we ran into before.
PropTypes
Default Props
In this other Product
component, we show how to use defaultProps
instead of conditional rendering.
import React from "react";
import PropTypes from "prop-types";
import defaultImage from "./assets/default-image.jpeg";
const Product = ({ image, name, price }) => {
return (
<article className="product">
{/**Use conditional rendering in case the data does not exist **/}
<img src={image.url} alt={name} />
<h4>{name}</h4>
<p>${price}</p>
</article>
);
};
// Define the propTypes for the object
Product.propTypes = {
image: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
};
Product.defaultProps = {
name: "default name",
price: 3.99,
image: defaultImage,
};
export default Product;
React Router
Links
How do we navigate through our application, well by using Links
. So, for example, in the Navbar
:
import React from 'react';
import { Link } from 'react-router-dom';
const Navbar = () => {
return (
<nav>
<ul>
<li>
<!-- Specify the path -->
<Link to='/'>Home</Link>
</li>
<li>
<!-- Specify the path -->
<Link to='/about'>About</Link>
</li>
<li>
<!-- Specify the path -->
<Link to='/people'>People</Link>
</li>
</ul>
</nav>
);
};
export default Navbar;
To pass a parameter to the link we can do the following:
import React, { useState } from 'react';
import { data } from '../../../data';
import { Link } from 'react-router-dom';
const People = () => {
// List of people
const [people, setPeople] = useState(data);
return (
<div>
<h1>People Page</h1>
{people.map((person) => {
return (
<div key={person.id} className='item'>
<h4>{person.name}</h4>
<!-- Specify the path and pass the id of the current person as a parameter -->
<Link to={`/person/${person.id}`}>Learn More</Link>
</div>
);
})}
</div>
);
};
export default People;
Now in the Person
component, we can fetch the parameter:
import React, { useState, useEffect } from 'react';
import { data } from '../../../data';
import { Link, useParams } from 'react-router-dom';
const Person = () => {
// State
const [name, setName] = useState('default name');
// useParams hook to fetch the parameter
// the name of the parameter (id), is specified in the "Route" component
// in our case the path to person was: /person/:id
const { id } = useParams();
useEffect(() => {
const newPerson = data.find((person) => person.id === parseInt(id));
setName(newPerson.name);
}, []);
return (
<div>
<h1>{name}</h1>
<!-- Go to the previous page of the list of people -->
<Link to='/people' className='btn'>
Back To People
</Link>
</div>
);
};
export default Person;