ref – https://reactjs.org/docs/hooks-custom.html
Problem
Say we have two functional components FriendStatus and FriendListItem. They both make use of a common functionality: check to see if a friend is online.
FriendStatus.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import React, { useState, useEffect } from 'react'; function FriendStatus(props) { //////////// check to see if a friend is online //////////// const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); ///////////////////////////////////// if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; } |
FriendListItem.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import React, { useState, useEffect } from 'react'; function FriendListItem(props) { ///////// check to see if friend is online ///////////// const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); //////////////////////////////////////////////////////// return ( <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li> ); } |
Let’s extract this friend checking online functionality into a custom hook
A custom Hook is a JavaScript function whose name starts with ”use”. It may call other Hooks
In our case, we create useFriendStatus function as a custom hook. We use other hooks within it. Just make sure to only call other Hooks unconditionally at the top level of your custom Hook: Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (ref – https://reactjs.org/docs/hooks-rules.html)
Hook Rules
Say we have a function Form. It uses 4 hooks: useState, useEffect, useState, and finally useEffect again.
Notice our 2 useStates.
First one is that we create a state and initialize it to ‘Mary’.
It creates a new Hook object (with the initial state ‘Mary’), adds the object to the Hooks list, and return the array with the initial state and the setter/getter functions (i.e name, setName).
We then repeat to create another state and initialize it to ‘Poppins’.
It creates a new Hook object (with the initial state ‘Poppins’), adds the object to the Hooks list, and return the array with the initial state and the setter/getter functions (i.e surname, setSurname).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function Form() { // 1. Use the name state variable const [name, setName] = useState('Mary'); // 2. Use an effect for persisting the form useEffect(function persistForm() { localStorage.setItem('formData', name); }); // 3. Use the surname state variable const [surname, setSurname] = useState('Poppins'); // 4. Use an effect for updating the title useEffect(function updateTitle() { document.title = name + ' ' + surname; }); // ... } |
Hence we know which state ‘Mary’ and ‘Poppins’ belong to because when creating them, we create a new Hook object and add it to the Hooks list. Thus, React relies on the order in which Hooks are called. Our example works because the order of the Hook calls is the same on every render.
For each render, React goes down the hook list and executes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// ------------ // First render // ------------ useState('Mary') // 1. Initialize the name state variable with 'Mary' useEffect(persistForm) // 2. Add an effect for persisting the form useState('Poppins') // 3. Initialize the surname state variable with 'Poppins' useEffect(updateTitle) // 4. Add an effect for updating the title // ------------- // Second render // ------------- useState('Mary') // 1. Read the name state variable (argument is ignored) useEffect(persistForm) // 2. Replace the effect for persisting the form useState('Poppins') // 3. Read the surname state variable (argument is ignored) useEffect(updateTitle) // 4. Replace the effect for updating the title // ... |
As long as the order of the Hook calls is the same between renders, React can associate some local state with each of them. But what happens if we put a Hook call (for example, the persistForm effect) inside a condition?
1 2 3 4 5 6 |
// We're breaking the first rule by using a Hook in a condition if (name !== '') { useEffect(function persistForm() { localStorage.setItem('formData', name); }); } |
The name !== ” condition is true on the first render, so we run this Hook. However, on the next render the user might clear the form, making the condition false. Now that we skip this Hook during rendering, the order of the Hook calls becomes different:
1 2 3 4 |
useState('Mary') // 1. Read the name state variable (argument is ignored) // useEffect(persistForm) // This Hook was skipped! useState('Poppins') // 2 (but was 3). Fail to read the surname state variable useEffect(updateTitle) // 3 (but was 4). Fail to replace the effect |
React wouldn’t know what to return for the second useState Hook call. React expected that the second Hook call in this component corresponds to the persistForm effect, just like during the previous render, but it doesn’t anymore. From that point, every next Hook call after the one we skipped would also shift by one, leading to bugs.
This is why Hooks must be called on the top level of our components. If we want to run an effect conditionally, we can put that condition inside our Hook:
1 2 3 4 5 6 |
useEffect(function persistForm() { // We're not breaking the first rule anymore if (name !== '') { localStorage.setItem('formData', name); } }); |
Unlike a React component, a custom Hook doesn’t need to have a specific signature. We can decide what it takes as arguments, and what, if anything, it should return. In other words, it’s just like a normal function. Its name should always start with use so that you can tell at a glance that the rules of Hooks apply to it.
useFriendStatus
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { useState, useEffect } from 'react'; function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; } |
Using the custom hook
Now that we’ve extracted this logic to a useFriendStatus hook, we can just use it:
FriendStatus.js
1 2 3 4 5 6 7 8 |
function FriendStatus(props) { const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; } |
FriendListItem.js
1 2 3 4 5 6 7 8 9 |
function FriendListItem(props) { const isOnline = useFriendStatus(props.friend.id); return ( <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li> ); } |
Since Hooks are functions, we can pass information between them
To illustrate this, we’ll use another component from our hypothetical chat example. This is a chat message recipient picker that displays whether the currently selected friend is online:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// data for array for the select control to display const friendList = [ { id: 1, name: 'Phoebe' }, { id: 2, name: 'Rachel' }, { id: 3, name: 'Ross' }, ]; function ChatRecipientPicker() { const [recipientID, setRecipientID] = useState(1); const isRecipientOnline = useFriendStatus(recipientID); // we have a select control that uses our array data to let user choose name and an id // once it has been chosen, it calls <b>setRecipientID</b>, which changes recipientID. // then recipientID, which injected into useFriendStatus, will get the status of this new friend. return ( <> <Circle color={isRecipientOnline ? 'green' : 'red'} /> <select value={recipientID} onChange={e => setRecipientID(Number(e.target.value))} > {friendList.map(friend => ( <option key={friend.id} value={friend.id}> {friend.name} </option> ))} </select> <> ) } |