React hooks were introduced in React v16.8
. Their most basic purpose is to give functions the ability to manage (and share) state, effects and much more while simultaneously simplifying expected behavior, increasing the potential for extensibility and improving performance.
This is meant to be a basic introduction to React hooks, so if all that jargon seems intimidating, don't worry, we'll go step by step into what it all means and how hooks can be useful to you.
Hopefully, this article can give you some insight into the power of React hooks, the methodology behind them, and at least a starting point to begin implementing them in your own projects.
React hooks reduce the number of components in the tree, significantly, making it easier to get a handle on the layout of the component tree and debug without having to wade through tons of Higher Order Components.
Hooks also continue with the general pattern that React has been edging towards for a while in that hooks allow, and encourage, the use of "vanilla" JavaScript by taking (more) advantage of closures and they allow more avenues to implement native JavaScript instead of adding more framework-specific abstractions.
No. As the React team states:
The React team has no current plans to deprecate classes.
"The React team has no current plans to deprecate classes or demand that you refactor all your current class components. They encourage a managed adoption strategy over time and hooks are completely backward compatible with existing React components, though there is the potential for some third-party libraries to have issues that arise as the adoption of hooks becomes more prevalent."
The useState
hook is similar to its counterpart with classes this.setState
but, in general, is meant for more granular state management and can contain any type, for managing larger state objects, arrays, etc., useReducer
is a better-suited hook.
The most common pattern used is array destructuring but if you are not familiar with that pattern, you can always just use the first two elements in the array.
The naming of both the state holder and the state changer are entirely up to you, but just as the convention with hooks is to start them with use
, the convention for the state setter is to start it with set
.
// State initialized with array destructuring
const [count, setCount] = useState(0);
// State initialized with regular array assignment
const countState = useState(0);
const count = countState[0];
const setCount = countState[1];
Here is a basic example that updates and displays a counter:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>The current count is {count}</p>
<button onClick={() => setCount(count + 1)}>}>Increment Counter</button>
</div>
);
}
Resources
This is another hook that you will see very often. It deals with side effects and runs after every completed render, but it is versatile enough to be used in place of componentDidMount
, componentDidUpdate
, componentWillUnmount
, and more.
Aside from giving you the opportunity to manage any side effects, there are a few other features that are important to notice with different implementations of useEffect
.
useEffect
can emulate the functionality of componentWillUnmount
by simply returning a function that cleans up after itself. In this returned function, you can unsubscribe from events that were subscribed to inside useEffect
or unreferenced anything that is no longer required. Like some of the other hooks, useEffect
can have a dependency array as the last parameter where the props, state or other kinds of values can be placed. This prevents unnecessary re-renders, as useEffect
will only run if one of its dependent values changes.
Since there is already a custom example of using useEffect
for the custom hook I created below, I will use the example from the React Docs below:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
As you can see, the return value of useEffect
here is a function used to clean up anything that is not wanted or needed after this component is unmounted. The other thing to notice is the dependency array containing props.friend.id
because we only want this useEffect
to run if that value changes.
Resources
The useContext
hook is just another way to access the context previously defined except it can now be used in a functional component. This still requires a Provider
of this specific context type to be somewhere above the component that useContext
is called in up the component tree.
You need to use the actual MyContext
object initialized with React.createContext
to as the parameter in useContext
—not MyContext.Provider
or MyContext.Consumer
.
Using useContext
//...
function functionThatUsesContext() {
const value = useContext(MyContext);
// read or subscribe to any of the values of MyContext object
}
Since the large majority of using Context is out of the scope of this hook, I have linked to everything related to using Context from the React Docs below (as it is a topic for a full article itself).
Resources
While useState
is an effective way to manage one or more simple state structures, useReducer
is designed to handle larger state structures that can be multiple levels deep.
You may be familiar with the concept of a reducer if you have worked with Redux. There are even some great articles on recreating a single Redux type store using useReducer
and various combinations of other hooks which I will link to below.
The example in the React Docs is a great basic introduction so I will use that as our basis:
Basic initialization
const [state, dispatch] = useReducer(reducer, initialArg, init);
useReducer
example
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
Just like Redux, the actions contained within the reducer
are dispatched by name with the dispatch
function and the reducer uses that information to perform an action on the state to create the new state. It can, of course, get more complicated the deeper your state structure goes you will need to remember to return the previous state along with the changes. This is usually done with Object Spread Syntax.
This example below shows how the someOtherStateVar
would not be changed when updating only the count:
const initialState = {count: 0, someOtherStateVar: [1,2,3]};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {
...state, // spreads the entire Object state into this object
count: state.count + 1 // overrides the count part of the previous state
};
case 'decrement':
return {
...state,
count: state.count + 1
};
default:
throw new Error();
}
}
Resources
The details on useCallback
in the React Docs is sparse, so here I'll just give some of the cliffnotes on the fantastic article written by Jan Hesters to clarify when to use useCallback
vs useMemo
and highly encourage you to read that article.
"
useCallback
gives you referential equality between renders for functions. AnduseMemo
gives you referential equality between renders for values." - Jan Hesters
useCallback
is for the cases when you need to call the callback function later, whereas useMemo
computes the value that can be referenced later. Both useCallback
and useMemo
only change when their dependency arrays change.
Basic useCallback
example
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
Resources
Simply put, useMemo
is used to memoize expensive operations so they are not repeated unless a dependency changes. The last parameter in useMemo
, like many other hooks, is the dependency array and this memoizedValue
will only be re-calculated if something changes in the dependency array.
Below is a quote from the React team on how to consider the use of useMemo
:
"You may rely on useMemo
as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance."
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Resources
useRef
useRef
creates a mutable JavaScript object that is useful and unique because it keeps the same reference to an object through multiple renders. The way to use useRef
is to mutate its { current: ... }
property. This will become more clear in the examples below.
Refs have been a concept in React for a while, but their main usage has been to refer to a specific DOM node in order to make mutations to that DOM node with any available functions on that node. This is still basically the case when using the useRef
hook, as it is possible to mutate DOM nodes, but useRef
has even more capabilities because it can also reference any value that needs to be accessed through any re-renders for the lifetime of the component. This makes it similar to the instance values that are commonly used in classes.
Initializing useRef
import React, { useRef } from 'react';
function someFunction() {
// ...
const refContainer = useRef(initialValue);
// ...
}
An important note to keep in mind when using useRef
is that mutating the refContainer.current
property does not cause a re-render. If you want to refer to a DOM node and make changes that cause a re-render, useCallback
is a better option.
Using useRef
to mutate a DOM node
//...
function TextInputWithBlurButton() {
const inputRef = useRef(null);
const blurOnButtonClick = () => {
// `current` points to the mounted text input element
inputRef.current.blur();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={blurOnButtonClick}>Click to blur input</button>
</>
);
}
Resources
useLayoutEffect
is essentially the same as useEffect
and has the same API. The main difference is that it runs synchronously unlike the asynchronous useEffect
so it is almost always recommended to use useEffect
to prevent unnecessary blocking of the browser updating the screen.
Some common uses for useLayoutEffect
are when you need to measure the DOM (screen position, scroll position, etc.) or mutate the DOM causing a synchronous re-render.
Resources
useDebugValue(value);
The main purpose of this hook, as its name suggests, is to help with debugging. It works with React Developer Tools to show the value of the hook that is useful for you to understand the state of the Component that your hook is managing.
useDebugValue(value, [optional formatting function]);
There is also an optional second parameter that is only called when inspecting the value in the developer tools. A good example from the React Documentation is formatting a date into a string on inspection in the developer tools.
useDebugValue(date, () => date.toDateString()); // The formatting function here is only called on dev tools inspection
This is more efficient than performing that same transformation outside of the useDebugValue()
hook because the toDateString()
function would be called every time the parent hook was called, instead of only when it's looked at in the React Dev Tools.
const formattedDate = date.toDateString(); // This is called on every hook run
useDebugValue(formattedDate);
Resources
All together now! Below I am going to explain a custom hook created to use setInterval
to aid in the creation of a very basic countdown clock.
Full disclosure, when I initially choose to do a countdown clock using setInterval
I found it somewhat difficult to work with. After some searching, the final pieces of this custom hook useInterval
were gleaned from Dan Abramov's blog post specifically on using setInterval in hooks and I encourage you to check it out for a more in-depth discussion of why setInterval
is particularly tricky to make declarative with React hooks.
If you would like to go straight to the working code, you can see it here on Code Sandbox.
Here is the actual custom hook useInterval
created for this:
import React, { useEffect, useState, useRef } from "react";
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Countdown
component that makes use of the useInterval
custom hook along with useRef
to keep the same reference to the initial date and useState
to hold both the countdownDuration
and the isRunning
state that the button uses to pause and un-pause the countdown.
Using the custom useInterval
hook
function Countdown() {
const initDateTime = useRef(Date.now());
const [countdownDuration, setCountdownDuration] = useState(
initDateTime.current + 123456789 // Random number choosen to have as a timer duration
);
const [isRunning, setIsRunning] = useState(true);
useInterval(
() => {
setCountdownDuration(countdownDuration - 1000);
},
isRunning ? 1000 : null
);
useInterval(() => {
if (countdownDuration < 1000) {
setIsRunning(false); // stop running countdown on the last second
}
}, 1000);
function handleChangeCountdown() {
setIsRunning(!isRunning); // Reverse the state of isRunning on each button click
}
return (
<div>
<CountdownClock
countdownDuration={countdownDuration}
initDateTime={initDateTime.current}
/>
{countdownDuration < 1000 && <h2>Countdown Reached!</h2>}
<button onClick={handleChangeCountdown}>
{isRunning ? "Pause Countdown" : "Continue Countdown"}
</button>
</div>
);
}
Since we don't need to update the state of the initial date, and we would like its value to remain consistent through each render, a useRef
is appropriate here as it functions as an instance variable in a class.
Here is the full Countdown.js
file if you didn't want to click over to the Code Sandbox Example:
Countdown.js
import React, { useEffect, useState, useRef } from "react";
import CountdownClock from "./CountdownClock";
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
function Countdown() {
const initDateTime = useRef(Date.now()); // since we don't need to change this value, we can use a ref
const [countdownDuration, setCountdownDuration] = useState(
initDateTime.current + 123456789 // Random number choosen for this
);
const [isRunning, setIsRunning] = useState(true);
useInterval(
() => {
setCountdownDuration(countdownDuration - 1000);
},
isRunning ? 1000 : null
);
useInterval(() => {
if (countdownDuration < 1000) {
setIsRunning(false);
}
}, 1000);
function handleChangeCountdown() {
setIsRunning(!isRunning);
}
return (
<div>
<h1>Learning React Hooks</h1>
<CountdownClock
countdownDuration={countdownDuration}
initDateTime={initDateTime.current}
/>
{countdownDuration < 1000 && <h2>Countdown Reached!</h2>}
<button onClick={handleChangeCountdown}>
{isRunning ? "Pause Countdown" : "Continue Countdown"}
</button>
</div>
);
}
export default Countdown;
If you are interested in how the CountdownClock
component is defined, here it is below, but it's not nearly as important to the subject matter of hooks as the previous two code blocks are on the definition of the useInterval
custom hook and the use of it in the Countdown
component.
CountdownClock.js
import React from "react";
import moment from "moment";
function getPluralOutput(duration, name) {
return duration > 0 && duration < 2
? `${duration} ${name}`
: `${duration} ${name}s`;
}
function CountdownClock({ countdownDuration, initDateTime }) {
const initMoment = moment(initDateTime);
const currentDuration = moment(countdownDuration);
const duration = moment.duration(currentDuration.diff(initMoment));
// Get initial values
let w = duration.weeks();
let d = duration.days();
let h = duration.hours();
let m = duration.minutes();
let s = duration.seconds();
// Set display values
w = getPluralOutput(w, "week");
d = getPluralOutput(d, "day");
h = getPluralOutput(h, "hour");
m = getPluralOutput(m, "minute");
s = getPluralOutput(s, "second");
return <h3>{`${w} ${d} ${h} ${m} ${s}`}</h3>;
}
export default CountdownClock;
Resources
While you can simply start using React Hooks if you have a project with React v16.8 or higher, this is modern JavaScript, so you know there is going to be some tooling to help guide syntax and best practices.
The React team recomments using their eslint-plugin-react-hooks
package with the exhaustive-deps
rule. If you are using Create React App (CRA), this plugin was included in react-scripts v3.0.0
or higher.
Whether using CRA or not, if you want these eslint rules to display in your editor of choice instead of just the terminal or in-browser (with CRA), you will still need to create an .eslintrc.json
(or .eslintrc
) file in your project and have the proper eslint plugin or extension for your editor.
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}