A high level overview of React Hooks
- Published on
- Authors
- Name
- Vasiliy Kramarenko
- @kramvas07
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
function updateWorkInProgressHook(): Hook {
// This function is used both for updates and for re-renders triggered by a
// render phase update. It assumes there is either a current hook we can
// clone, or a work-in-progress hook from a previous render pass that we can
// use as a base. When we reach the end of the base list, we must switch to
// the dispatcher used for mounts.
let nextCurrentHook: null | Hook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
useState
The useState Hook can be called inside a function component to add some local state to it. React will preserve this state between renders. useState returns an array with two items:
- the current state value
- a function that lets you update it (e.g. to be called from an event handler)
The only argument to useState is the initial state.
One important difference between this.setState in class components is that with Hooks updating a state variable always replaces it instead of merging it.
Here's an example of a simple search input:
import React, { useState } from 'react';
const Search = () => {
const [searching, setSearching] = useState(false);
const [keywords, setKeywords] = useState('');
return (
<div className="search">
<input type="search" value={keywords} onChange={e => setKeywords(e.target.value)} />
<button onClick={() => setSearching(true)}>Search</button>
</div>
);
};
export default Search;
We're using the useState hook twice, to set the keywords to state as the user types in, and set searching boolean to true once the user clicks the search button.
But how do we perform an actual search?
Source code useState packages/react-reconciler/src/ReactFiberHooks.new.js
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
useEffect
The useEffect Hook adds the ability to perform side effects from a function component. It serves the same purpose as componentDidMount, componentDidUpdate and componentWillUnmount in class based components, but unified into a single API.
By default, React runs the effects after every render, including the initial one. React guarantees the DOM has been updated by the time it runs the effects.
For our search example, we can use the useEffect Hook to perform the search, but we need to check whether the user clicked the search button before searching, otherwise the search would be performed every time the component rerenders (in this case every time the user types in something into the search input).
import React, { useState, useEffect } from 'react';
const Search = () => {
const [searching, setSearching] = useState(false);
const [keywords, setKeywords] = useState('');
useEffect(() => {
if (searching && keywords != '') {
console.log('Search for keywords: ', keywords);
// this is where we would call an API to perform the search
return () => {
setSearching(false);
};
}
});
return (
<div className="search">
<input type="search" value={keywords} onChange={e => setKeywords(e.target.value)} />
<button onClick={() => setSearching(true)}>Search</button>
</div>
);
};
export default Search;
useEffect has a nice feature to perform a cleanup after running an effect. If an effect returns a function, React will run it when it's time to clean up. In our search example, we use it to set searching back to false.
React allows us to skip an effect unless a prop changes. We don't need this feature on our search example, but it would work something like this:
useEffect(() => {
// call some API
}, [props.id]); // run the effect only if the id changes
Passing an empty array [] of inputs tells React that your effect doesn't depend on any values from the component, so it would run only on mount and clean up on unmount, it wound't run on updates.
Note: You can include multiple useEffect functions in a component and React will run each one, in the order they were specified.
Source code useEffect packages/react-reconciler/src/ReactFiberHooks.new.js
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
if (__DEV__) {
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
}
}
if (
__DEV__ &&
enableStrictEffects &&
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
) {
return mountEffectImpl(
MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
} else {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
}
}
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
if (__DEV__) {
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
}
}
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
useLayoutEffect
The useLayoutEffect Hook is identical to useEffect, but it fires synchronously after all DOM mutations. You can use it to to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.
Basically, useEffect fires after layout and paint, during a deferred event and doesn't block the browser from updating the screen, while useLayoutEffect fires before and you should use it only if you need to ensure that DOM mutations fire synchronously before the next paint so that the user does not perceive a visual inconsistency.
Source code useLayoutEffect packages/react-reconciler/src/ReactFiberHooks.new.js
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
let fiberFlags: Flags = UpdateEffect;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStaticEffect;
}
if (
__DEV__ &&
enableStrictEffects &&
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
) {
fiberFlags |= MountLayoutDevEffect;
}
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
function updateLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}
useContext
The useContext Hook lets us subscribe to React context without introducing nesting:
function Example() {
const locale = useContext(LocaleContext);
const theme = useContext(ThemeContext);
}
useContext accepts a context object (the value returned from React.createContext) and returns the current context value, as given by the nearest context provider for the given context.
When the provider updates, the useContext Hook will trigger a rerender with the latest context value.
useReducer
The useReducer Hook lets us manage local state of complex components with a reducer:
function Example() {
const [todos, dispatch] = useReducer(todosReducer);
}
It's an alternative to useState.
useReducer accepts a reducer of type (state, action) => newState and returns the current state paired with a dispatch method.
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. It also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
useCallback
The useCallback Hook returns a memoized callback:
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
You pass an inline callback and an array of inputs. useCallback will return a memoized version of the callback that only changes if one of the inputs has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).
Source code useCallback packages/react-reconciler/src/ReactFiberHooks.new.js
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
useMemo
The useMemo Hook returns a memoized value. You pass a create function and an array of inputs.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Source code useMemo packages/react-reconciler/src/ReactFiberHooks.new.js
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
useRef
The useRef Hook returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
const refContainer = useRef(initialValue);
A common use case is to access a child imperatively. useRef is useful for more than the ref attribute, it is handy for keeping any mutable value around similar to how you'd use instance fields in classes.
packages/shared/ReactTypes.js
export type RefObject = {|
current: any,
|};
Source code useRef packages/react-reconciler/src/ReactFiberHooks.new.js
function mountRef<T>(initialValue: T): {| current: T |} {
const hook = mountWorkInProgressHook();
if (enableUseRefAccessWarning) {
const ref = { current: initialValue };
hook.memoizedState = ref;
return ref;
} else {
const ref = { current: initialValue };
hook.memoizedState = ref;
return ref;
}
}
function updateRef<T>(initialValue: T): {| current: T |} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
useImperativeHandle
The useImperativeHandle Hook customizes the instance value that is exposed to parent components when using ref.
useImperativeHandle(ref, createHandle, [inputs]);
useDebugValue
The useDebugValue Hook can be used to display a label for custom hooks in React DevTools:
useDebugValue(value);
It's not recommended to add debug values to every custom Hook. It's most valuable for custom Hooks that are part of shared libraries.
Hooks Rules
There are two important rules with Hooks:
- Only call Hooks at the top level - don’t call them inside loops, conditions, or nested functions.
- Only call Hooks from React function components - don’t call them from regular JavaScript functions. (There is just one other valid place to call Hooks — your own custom Hooks)
Creating custom Hooks
By creating custom Hooks you can extract component logic into reusable functions.
Custom Hooks are more of a convention than a feature. If a function's name starts with use and it calls other Hooks, React will know that it's a custom Hook.
Let's refactor our original search example to remove the search button and instead search when the visitor presses the *Enter key on their keyboard.
First let's create a custom useKeyPress hook that will check which keyboard key the visitor pressed:
function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = useState(false);
function downHandler({ key }) {
if (key === targetKey) {
setKeyPressed(true);
}
}
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
useEffect(() => {
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, []);
return keyPressed;
}
Our custom hook accepts a single parameter, the key that we're expecting to be pressed. We created two event handler functions and added event listeners inside the useEffect Hook. Notice that we passed an empty array [] as the second argument to useEffect to ensure it only runs on mount and unmount. We're also removing event listeners on cleanup.
Now we can use the useKeyPress Hook in our Search component:
const Search = () => {
const [keywords, setKeywords] = useState('');
let pressedEnter = useKeyPress('Enter');
useEffect(() => {
if (pressedEnter && keywords != '') {
console.log('Search for keywords: ', keywords);
// this is where we call an api to perform the search
}
}, [pressedEnter]);
return (
<div className="search">
<input type="search" value={keywords} onChange={e => setKeywords(e.target.value)} />
</div>
);
};
export default Search;
We removed the search button and searching state variable and only checking whether the visitor pressed Enter. We also added pressedEnter to the array of inputs to skip the effect unless the pressedEnter variable changes.
I hope this was helpful to get you started with React Hooks. Next check out the official Hooks documentation and other Hooks tutorials.