Hooks are a new addition in React that lets you use state and other React features without writing a class. This website provides easy to understand code examples to help you learn how hooks work and inspire you to take advantage of them in your next project.
One question that comes up a lot is "When using hooks how do I get the previous value of props or state?". With React class components you have the componentDidUpdate method which receives previous props and state as arguments or you can update an instance variable (this.previous = value) and reference it later to get the previous value. So how can we do this inside a functional component that doesn't have lifecycle methods or an instance to store values on? Hooks to the rescue! We can create a custom hook that uses the useRef hook internally for storing the previous value. See the recipe below with inline comments. You can also find this example in the official React Hooks FAQ.
import { useState, useEffect, useRef } from "react";
// Usage
function App() {
// State value and setter for our example
const [count, setCount] = useState(0);
// Get the previous value (was passed into hook on last render)
const prevCount = usePrevious(count);
// Display both current and previous count value
return (
<div>
<h1>
Now: {count}, before: {prevCount}
</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// Hook
function usePrevious(value) {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref = useRef();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
import { useState, useEffect, useRef } from "react";
// Usage
function App() {
// State value and setter for our example
const [count, setCount] = useState<number>(0);
// Get the previous value (was passed into hook on last render)
const prevCount: number = usePrevious<number>(count);
// Display both current and previous count value
return (
<div>
<h1>
Now: {count}, before: {prevCount}
</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// Hook
function usePrevious<T>(value: T): T {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref: any = useRef<T>();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
This hook allows you to detect clicks outside of a specified element. In the example below we use it to close a modal when any element outside of the modal is clicked. By abstracting this logic out into a hook we can easily use it across all of our components that need this kind of functionality (dropdown menus, tooltips, etc).
import { useState, useEffect, useRef } from "react";
// Usage
function App() {
// Create a ref that we add to the element for which we want to detect outside clicks
const ref = useRef();
// State for our modal
const [isModalOpen, setModalOpen] = useState(false);
// Call hook passing in the ref and a function to call on outside click
useOnClickOutside(ref, () => setModalOpen(false));
return (
<div>
{isModalOpen ? (
<div ref={ref}>
👋 Hey, I'm a modal. Click anywhere outside of me to close.
</div>
) : (
<button onClick={() => setModalOpen(true)}>Open Modal</button>
)}
</div>
);
}
// Hook
function useOnClickOutside(ref, handler) {
useEffect(
() => {
const listener = (event) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
},
// Add ref and handler to effect dependencies
// It's worth noting that because passed in handler is a new ...
// ... function on every render that will cause this effect ...
// ... callback/cleanup to run every render. It's not a big deal ...
// ... but to optimize you can wrap handler in useCallback before ...
// ... passing it into this hook.
[ref, handler]
);
}
This hook allows you to smoothly animate any value using an easing function (linear, elastic, etc). In the example we call the useAnimation hook three times to animated three balls on to the screen at different intervals. Additionally we show how easy it is to compose hooks. Our useAnimation hook doesn't actual make use of useState or useEffect itself, but instead serves as a wrapper around the useAnimationTimer hook. Having the timer logic abstracted out into its own hook gives us better code readability and the ability to use timer logic in other contexts. Be sure to check out the CodeSandbox Demo for this one.
import { useState, useEffect } from "react";
// Usage
function App() {
// Call hook multiple times to get animated values with different start delays
const animation1 = useAnimation("elastic", 600, 0);
const animation2 = useAnimation("elastic", 600, 150);
const animation3 = useAnimation("elastic", 600, 300);
return (
<div style={{ display: "flex", justifyContent: "center" }}>
<Ball
innerStyle={{
marginTop: animation1 * 200 - 100,
}}
/>
<Ball
innerStyle={{
marginTop: animation2 * 200 - 100,
}}
/>
<Ball
innerStyle={{
marginTop: animation3 * 200 - 100,
}}
/>
</div>
);
}
const Ball = ({ innerStyle }) => (
<div
style={{
width: 100,
height: 100,
marginRight: "40px",
borderRadius: "50px",
backgroundColor: "#4dd5fa",
...innerStyle,
}}
/>
);
// Hook
function useAnimation(easingName = "linear", duration = 500, delay = 0) {
// The useAnimationTimer hook calls useState every animation frame ...
// ... giving us elapsed time and causing a rerender as frequently ...
// ... as possible for a smooth animation.
const elapsed = useAnimationTimer(duration, delay);
// Amount of specified duration elapsed on a scale from 0 - 1
const n = Math.min(1, elapsed / duration);
// Return altered value based on our specified easing function
return easing[easingName](n);
}
// Some easing functions copied from:
// https://github.com/streamich/ts-easing/blob/master/src/index.ts
// Hardcode here or pull in a dependency
const easing = {
linear: (n) => n,
elastic: (n) =>
n * (33 * n * n * n * n - 106 * n * n * n + 126 * n * n - 67 * n + 15),
inExpo: (n) => Math.pow(2, 10 * (n - 1)),
};
function useAnimationTimer(duration = 1000, delay = 0) {
const [elapsed, setTime] = useState(0);
useEffect(
() => {
let animationFrame, timerStop, start;
// Function to be executed on each animation frame
function onFrame() {
setTime(Date.now() - start);
loop();
}
// Call onFrame() on next animation frame
function loop() {
animationFrame = requestAnimationFrame(onFrame);
}
function onStart() {
// Set a timeout to stop things when duration time elapses
timerStop = setTimeout(() => {
cancelAnimationFrame(animationFrame);
setTime(Date.now() - start);
}, duration);
// Start the loop
start = Date.now();
loop();
}
// Start after specified delay (defaults to 0)
const timerDelay = setTimeout(onStart, delay);
// Clean things up
return () => {
clearTimeout(timerStop);
clearTimeout(timerDelay);
cancelAnimationFrame(animationFrame);
};
},
[duration, delay] // Only re-run effect if duration or delay changes
);
return elapsed;
}
A really common need is to get the current size of the browser window. This hook returns an object containing the window's width and height. If executed server-side (no window object) the value of width and height will be undefined.
import { useState, useEffect } from "react";
// Usage
function App() {
const size = useWindowSize();
return (
<div>
{size.width}px / {size.height}px
</div>
);
}
// Hook
function useWindowSize() {
// Initialize state with undefined width/height so server and client renders match
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
}
import { useState, useEffect } from "react";
// Define general type for useWindowSize hook, which includes width and height
interface Size {
width: number | undefined;
height: number | undefined;
}
// Usage
function App() {
const size: Size = useWindowSize();
return (
<div>
{size.width}px / {size.height}px
</div>
);
}
// Hook
function useWindowSize(): Size {
// Initialize state with undefined width/height so server and client renders match
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState<Size>({
width: undefined,
height: undefined,
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
}
Detect whether the mouse is hovering an element. The hook returns a ref
and a boolean value indicating whether the element with that ref is currently being
hovered. Just add the returned ref to any element whose hover state you want
to monitor. One potential bug with this method: If you have logic that changes the element that hoverRef
is added to then your event listeners will not necessarily get applied to the new element. If you need this functionality then use this alternate version that utilizes a callback ref.
// Usage
function App() {
const [hoverRef, isHovered] = useHover();
return <div ref={hoverRef}>{isHovered ? "😁" : "☹️"}</div>;
}
// Hook
function useHover() {
const [value, setValue] = useState(false);
const ref = useRef(null);
const handleMouseOver = () => setValue(true);
const handleMouseOut = () => setValue(false);
useEffect(
() => {
const node = ref.current;
if (node) {
node.addEventListener("mouseover", handleMouseOver);
node.addEventListener("mouseout", handleMouseOut);
return () => {
node.removeEventListener("mouseover", handleMouseOver);
node.removeEventListener("mouseout", handleMouseOut);
};
}
},
[ref.current] // Recall only if ref changes
);
return [ref, value];
}
// Usage
function App() {
const [hoverRef, isHovered] = useHover<HTMLDivElement>();
return <div ref={hoverRef}>{isHovered ? "😁" : "☹️"}</div>;
}
// Hook
// T - could be any type of HTML element like: HTMLDivElement, HTMLParagraphElement and etc.
// hook returns tuple(array) with type [any, boolean]
function useHover<T>(): [MutableRefObject<T>, boolean] {
const [value, setValue] = useState<boolean>(false);
const ref: any = useRef<T | null>(null);
const handleMouseOver = (): void => setValue(true);
const handleMouseOut = (): void => setValue(false);
useEffect(
() => {
const node: any = ref.current;
if (node) {
node.addEventListener("mouseover", handleMouseOver);
node.addEventListener("mouseout", handleMouseOut);
return () => {
node.removeEventListener("mouseover", handleMouseOver);
node.removeEventListener("mouseout", handleMouseOut);
};
}
},
[ref.current] // Recall only if ref changes
);
return [ref, value];
}
Sync state to local storage so that it persists through a page refresh. Usage is similar to useState except we pass in a local storage key so that we can default to that value on page load instead of the specified initial value.
import { useState } from "react";
// Usage
function App() {
// Similar to useState but first arg is key to the value in local storage.
const [name, setName] = useLocalStorage("name", "Bob");
return (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
// Hook
function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue];
}
import { useState } from "react";
// Usage
function App() {
// Similar to useState but first arg is key to the value in local storage.
const [name, setName] = useLocalStorage<string>("name", "Bob");
return (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
// Hook
function useLocalStorage<T>(key: string, initialValue: T) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value: T | ((val: T) => T)) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue] as const;
}