React Hooks
willp11
Posted on August 19, 2022
Introduction
React Hooks are the modern way of writing React applications. Initially, React components that required the use of any state were always class-based as functional components did not have a way of storing state. However, there were some pain points with using classes that led to hooks being released in React 16.8, which hoped to address these pain points by enabling the use of state and other features within functional components. This tutorial will look at the main features of React hooks, demonstrating the various hooks that are available and some use cases for each.
Managing State
I will demonstrate three hooks that can be used to manage state within functional components; useState, useReducer and useContext.
useState
useState
The useState hook is the simplest of all the hooks. It enables the storage of simple state variables within a component. Whenever you require some simple state to be stored, you should always turn to useState as your first option.
The useState hook returns an array containing the variable that will hold the state and a function that updates that variable. It is common to name the function set[Variable].
const [counter, setCounter] = useState();
return (
<>
<p>{counter}</p>
<button onClick={()=>setCounter(counter+1)}>Increment</button>
<>
)
You cannot mutate the variable without using the function returned from useState that is designated for that purpose. The example below is not valid React code.
let [counter, setCounter] = useState();
counter += 1;
useReducer
The useReducer hook should be used when you have more complex state, as by using useReducer you can define multiple functions to update the state.
First, you need to define the reducer. The reducer is a function that takes the state and an action as inputs and mutates the state depending on the type of the action.
Here is a simple example of adding and removing items from a shopping cart.
const addItem = (state, item) => {
return {
…state,
[item.name]: item
}
}
const removeItem = (state, item) => {
let cart = {…state};
delete cart[item.name];
return cart;
}
const cartReducer = (state, action) => {
switch (action.type) {
case ‘ADD_ITEM’:
return addItem(state, action.item);
case ‘REMOVE_ITEM’:
return removeItem(state, action.item);
default:
return state;
}
}
Once you have defined the reducer, use the useReducer hook to access and update the state.
const [cart, dispatch] = useReducer(cartReducer);
let item = {name: t-shirt, price: 10};
dispatch({type: ‘ADD_ITEM’, item});
useContext
Context allows us to manage global state within a React app without the use of any 3rd party libraries such as Redux.
To use context, you need to create a context object using React.createContext(). The context object has a provider property which in turn takes a “value” prop. Store the state you wish to use globally as the “value” prop.
const CartContext = React.createContext();
const CartProvider = ({children}) => {
const [cart, dispatch] = useReducer(cartReducer);
return <CartContext.Provider value={[cart,dispatch]}>{children}</CartContext.Provider>
}
Wrap the provider around any components that need to use the state.
<CartProvider><AnotherComponent /><CartProvider>
Then use the useContext hook to access the state within those components.
const AnotherComponent = () => {
const [cart, dispatch] = useContext(CartContext);
…
}
useEffect
The useEffect hook is a very important hook as it essentially replaces the component lifecycle methods that were previously used in class-based components. Anything that you previously did with componentDidMount, componentDidUpdate and componentWillUnmount can now be done with the useEffect hook.
Side effects such as mutations, subscriptions, timers and logging must not be done within the main body of a functional component (React’s render phase), as it can cause confusing bugs and inconsistencies within the UI. Instead, useEffect will run after the render phase.
The useEffect hook takes a function and an optional dependency array as inputs. By default, the function defined within useEffect will run after every render (if no dependency array is provided). To adjust when the useEffect hook runs, we adjust the values within the dependency array. An empty dependency array means the effect will run just once when the component mounts.
No dependency array: runs after every render.
useEffect(()=>{
…
});
Empty dependency array: runs once after first render.
useEffect(()=>{
…
}, []);
Has dependencies: runs whenever any of the dependencies changes.
useEffect(()=>{
…
}, [dep1, dep2]);
One of the most common use-cases for useEffect is to retrieve data from an external API. For example, a component with a list of products may need to retrieve all the product data when the component mounts. Note that you cannot define useEffect as async, therefore to use await you must define the async function within useEffect then call it within that same useEffect.
const [products, setProducts] = useState();
useEffect(()=>{
const getProducts = async () => {
const res = await fetch(‘/products’);
const products = res.json();
setProducts(products);
}
getProducts();
}, [])
To avoid memory leaks when using useEffect for a subscription, you may need to unsubscribe when the component unmounts. To do this, use a return statement within the useEffect to perform any clean up that is required.
In the example below, a counter starts when the component mounts and then is cleared after the component unmounts.
const [count, setCount] = useState(0);
useEffect(()=>{
const counter = setInterval(()=>setCount(count=>count+1), 1000);
return () => clearInterval(counter);
}, [])
In the situation above, you must be careful to use setCount(count=>count+1) and NOT setCount(count+1). This is because if you do not pass a callback function to setCount, it will use a stale reference and be forever stuck on 1. In this case, the console will give a warning that you are missing count as a dependency. If you put count into the dependency array, it will give the appearance that you have solved the problem, however in reality you will be creating a brand new interval every second as useEffect gets called again and again.
Another common use case is to update a component once a user has logged in. For example, a user logs in and receives an authentication token that enables them to retrieve restricted data from an API, such as data about their user profile. In this case, we want to fetch the user profile data once the auth token changes.
const [authState, dispatch] = useContext(AuthContext);
const [user, setUser] = useState();
useEffect(()=>{
const getUser = async () => {
const res = await fetch(‘/getUser’, {headers: {‘Authorization’: ‘Token ’ + authState.token}}
setUser(res.json());
}
getUser();
}, [authState.token])
useRef
The last hook I will cover in this post is useRef. The useRef hook has two main uses; access DOM elements directly and when you want a mutable value that persists for the entire lifecycle of the component but which doesn't cause the component to re-render. To access the value in the ref, you use the ref's current property.
A common example for when you want to access DOM elements directly, is to click on something and then cause another component to gain focus.
const inputRef = useRef();
const focusInput = () => {
inputRef.current.focus();
}
return (
<>
<input type="text" ref={inputRef}/>
<button onClick={focusInput}>Focus</button>
</>
)
If you ever have a situation where you need to keep a value between re-renders but mutating that value doesn't need to cause a re-render, then consider using useRef instead of useState.
That's it for this article. Next time, I will discuss useRef, useCallback and writing your own custom hooks.
Posted on August 19, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024