Joshua's Docs - React Hooks - Cheat Sheet for Built-In Hooks and Custom
Light
help

What are React Hooks?

React hooks partially came about because of, and an alternative to, the complexity and boilerplate involved with class based components (as opposed to functions) and/or HOCs (higher order components). They are not required to be used, but many new devs prefer them for readability and isolation.

At their core, the key value proposition of React hooks is that they allow you to hook more directly into the React lifecycle and core APIs, such as state, props, etc.

Built-In React Hooks

Hook What
useState Get and set state
useEffect Wrap a function that contains possibly effectful code, which will be called after render.

You can conditionally trigger it by passing a dependency array as the second argument.

You can use it as a cleanup method (before component unmounts), by returning a cleanup function.
useContext Returns the current value for a given context object (object from React.createContext). Value is from nearest Provider.

A context value change will always trigger a re-render.

Using this with a context provider is a common alternative to prop-drilling, and/or large global state management systems.
useReducer An alternative to useState that basically takes a function to mutate state, rather than the state update directly.
useCallback Meant for optimization, you pass a callback and the dependencies that the callback uses to produce a result, and you get back a memoized (i.e. cached) callback function that only changes if the dependencies change.
useMemo Close to useCallback, but returns a memoized value, rather than a callback, that is only recomputed if the dependencies change.

Good use case is for computed values derived from state.
useRef Returns a special mutable reference object, which can be updated, but persists across re-renders and does not trigger re-renders on change.

Value can be accessed with .current.
useImperativeHandle Lets you modify the reference that is passed to parent components (for example, to add a method onto ref.current). If used, should be combined with forwardRef, but also not recommended, period.
useLayoutEffect Essentially the same as useEffect, but fires after all DOM updates have occurred (e.g. at componentDidMount, or componentDidUpdate)
useDebugValue For custom hooks, you can use this to display a special label for the hook inside the React DevTools.

TypeScript Concerns

Be aware that TypeScript can be particular about how you use hooks and annotate types. For example, for useState, you should pass the type through a generic slot if it is anything other than a primitive:

interface Book {
	title: string;
	copyrightYear: number;
}
const [books, setBooks] = useState<Book[]>([]);
// Types for `books` and `setBooks` can now be determined

const [counter, setCounter] = useState(0);
// This is fine, because `number` can be inferred

Building Your Own Hooks

You can easily write, share, and reuse custom hooks across many different components. In fact, this is one of the main selling points of hooks:

Hooks allow you to reuse stateful logic without changing your component hierarchy.

To create and re-use a hook, simply define it as an exported function, and then import where you need to use it. See the docs for details.

Custom React Hook - Sample Syntax

Remember to prefix with use.

Here is a general template:

export function useMyHook() {
	// Instantiate state vars
	const [myHookState, setMyHookState] = useState(null);

	// Setup code to update the hook state and handle removal
	useEffect(() => {
		// Register things (e.g. listeners, subscriptions, etc) to call setMyHookState
		return () => {
			// Un-register things that will call setMyHookState
		}
	});

	// Don't forget to return the state / val
	return myHookState;
}

Custom React Hooks - Examples

React Hook - Window Dimensions
export const useWindowSize = () => {
	const [size, setSize] = useState([0, 0]);
	useLayoutEffect(() => {
		// Need to hold function reference so we can use it for cleanup
		const updateSize = () => {
			setSize([window.innerWidth, window.innerHeight]);
		};
		window.addEventListener('resize', updateSize);
		updateSize();
		return () => window.removeEventListener('resize', updateSize);
	}, []);
	return size;
};

Chaining Hooks

You can "chain" hooks, where one hook update triggers another, by pulling them in as dependencies (second argument to useEffect).

For example, if I've already created a useWindowSize hook that updates when the window is resized, I could reuse that hook as the trigger to re-calculate the size of an element on the page, like so:

export const useElemSize = (elementRef) => {
	const [size, setSize] = useState({ width: 0, height: 0 });

	// Use windowSize hook as trigger to re-evaluate
	const windowSize = useWindowSize();

	useEffect(() => {
		try {
			const elem = elementRef.current;
			/** @type {DOMRect} */
			const domRect = elem.getBoundingClientRect();
			setSize({
				width: domRect.width,
				height: domRect.height
			});
		} catch (e) {
			setSize({ width: 0, height: 0 });
		}
	}, [windowSize, elementRef]);

	return size;
};

Problem Areas

useEffect Dependencies

One major problem area with React hooks that I've encountered is the dependency array of useEffect. Even with things like eslint-plugin-react-hooks and the bundled react-hooks/exhausitve-deps rule, it is a little too easy to run into confusing cases.

useEffect Dependencies - Outside Callbacks

One of the very first mistakes I made with React hooks was something like this:

// This is bad code

const MyComponent = () => {

	const internalHandler = () => {
		// bunch of code
		// something that touches state or props and *causes re-render*:
	}

	useEffect(() => {
		// register handler once
		window.setInterval(internalHandler, 1000);
	}, [internalHandler])
	
	return (
		// Assume some jsx
	)
}

The big issue with the above is that I've accidentally created an infinite loop.

  • Whenever MyComponent is called (rendered), a new internalHandler is defined
  • Because internalHandler is a dependency for useEffect, it getting created on render is triggering it and causing the handler to get registered again
  • internalHandler does something to state / props and triggers a re-render, which starts the loop all over again

The ESLint hooks rule will actually catch this, and warn with something like:

⚠ The 'internalHandler' function makes the dependencies of useEffect Hook (at line ___) change on every render. Move it inside the useEffect callback. Alternatively, wrap the 'internalHandler' definition into its own useCallback() Hook

That looks like a helpful error! Using either of the solutions it mentions should work. But... what if our case is complex, and wrapping it in useCallback still updates a part of state that it itself needs? Here is an example:

export const MyComponent = () => {
	const [counter, setCounter] = useState(0);

	const internalHandler = useCallback(() => {
		// something that touches state and *causes re-render*
		setCounter(counter + 1);
	}, [counter]);

	useEffect(() => {
		// register handler once
		const timerId = window.setInterval(internalHandler, 1000);
		console.log(`Registered handler #${timerId}`);
	}, [internalHandler]);

	return <div>It has been {counter} seconds!</div>;
};

On the surface, this might look OK; we are using useCallback to avoid recreating the callback on re-renders, and there are no eslint errors about exhaustive dependencies... but... we just did it again! We created another circular infinite loop!

  • counter is a state value, so changing it will cause a re-render
  • Since counter is a dependency of internalHandler, changing it also means that React thinks the handler is stale, and will create a new instance
  • React creating a new instance of internalHandler is basically just as bad as if we had declared it without using useCallback(); we are back to square one, where useEffect runs on every render and keeps registering new (separate) handlers

Solution? Here are some approaches:

  • Set state variables in callbacks, but don't use the same ones you are setting!

    • If the new state depends on the old one, like our counter example, an easy workaround is to use the alternative syntax for the state setter:

      // Rewrite this:
      const internalHandler = useCallback(() => {
      	setCounter(counter + 1);
      }, [counter]);
      
      // To this:
      const internalHandler = useCallback(() => {
      	setCounter(counter => counter + 1);
      }, []);
      // ^ a state variable is no longer a dependency
  • Use ref instead of state, where appropriate

    • Warning: a change to a ref value will not trigger a re-render by itself. So although this would fix the circular issue in our example, the DOM would continue to show It has been 0 seconds no matter how much time has actually passed. Here is how we could rewrite our example to use a ref:

      export const MyComponent = () => {
      	const [elapsedSec, setElapsedSec] = useState(0);
      	const startTime = useRef(new Date().getTime());
      
      	const internalHandler = useCallback(() => {
      		// something that touches state and *causes re-render*
      		const now = new Date().getTime();
      		setElapsedSec(Math.floor((now - startTime.current) / 1000));
      	}, []);
      
      	useEffect(() => {
      		// register handler once
      		const id = window.setInterval(internalHandler, 1000);
      		console.log(`Registered handler ${id}`);
      	}, [internalHandler]);
      
      	return <div>It has been {elapsedSec} seconds!</div>;
      };
  • Avoid using state entirely, for something that is simply a computed property
  • When possible, move callbacks directly inside useEffect, so they don't have to be dependencies and can live inside the right closure

useCallback Infinite Loop

It is also crazy easy to accidentally repeat the exact same circular issue with the dependency array of useCallback as outlined above with `useEffect, when trying to use a variable that is scoped to the outer closure:

export const MyComponent = props => {
	const [elapsedSec, setElapsedSec] = useState(0);
	const startTime = useRef(new Date().getTime());

	// In case callback is not defined in props
	const twoSecCallback =
		props.twoSecCallback ||
		(() => {
			console.log(`2 seconds are up!`);
		});

	const internalHandler = useCallback(() => {
		// something that touches state and *causes re-render*
		const now = new Date().getTime();
		const elapsedSec = Math.floor((now - startTime.current) / 1000);
		if (elapsedSec === 2) {
			twoSecCallback();
		}
		setElapsedSec(Math.floor((now - startTime.current) / 1000));
	}, [twoSecCallback]);

	useEffect(() => {
		// register handler once
		const id = window.setInterval(internalHandler, 1000);
		console.log(`Registered handler ${id}`);
	}, [internalHandler]);

	return <div>It has been {elapsedSec} seconds!</div>;
};

There are no ESlint hook warnings, but the above will indeed cause issues due to what is essentially the same problem as outlined in the above section on callbacks outside useEffect:

  • twoSecCallback being declared outside the closure of useCallback means that every time the component re-renders, it changes
  • Since twoSecCallback is a dependency of internalHandler, when the callback variable changes, the useCallback wrapper (internalHandler) is deemed to be stale, and React will create a new version
  • React creating a new version of the useCallback closure (internalHandler) means that, since it is a dependency of useEffect, the effect function will run again
  • The call stack on subsequent renders ends up looking like render() -> useEffect runs due to (CHANGE) in internalHandler, registers new internalHandler -> twoSecCallback {CHANGE} -> internalHandler {CHANGE} -> (NEXT RENDER) -> useEffect runs due to... and so on. Until you have hundreds of duplicate timers, handlers, and a crashed browser tab 🔥💥😬

The solution here is similar to the solutions with useEffect. The easiest solution in this case is simply to move the callback variable declaration within the closure of useCallback:

export const MyComponent = props => {
	const [elapsedSec, setElapsedSec] = useState(0);
	const startTime = useRef(new Date().getTime());

	const internalHandler = useCallback(() => {
		// In case callback is not defined in props
		const twoSecCallback =
			props.twoSecCallback ||
			(() => {
				console.log(`2 seconds are up!`);
			});
		// something that touches state and *causes re-render*
		const now = new Date().getTime();
		const elapsedSec = Math.floor((now - startTime.current) / 1000);
		if (elapsedSec === 2) {
			twoSecCallback();
		}
		setElapsedSec(Math.floor((now - startTime.current) / 1000));
	}, []);

	useEffect(() => {
		// register handler once
		const id = window.setInterval(internalHandler, 1000);
		console.log(`Registered handler ${id}`);
	}, [internalHandler]);

	return <div>It has been {elapsedSec} seconds!</div>;
};
Markdown Source Last Updated:
Mon Oct 12 2020 05:25:00 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Mon Aug 31 2020 09:49:09 GMT+0000 (Coordinated Universal Time)
© 2020 Joshua Tzucker, Built with Gatsby
Feedback