By Andrew Grosner · Mar 16 · 5 min read
React Hooks are a way to use features that typically were only available to class components in functional components. It adds the ability to useState
, useContext
, and many other features not only without the need to create class components — but also to reuse common behavior in a functional way. You can also write your own custom hooks, introducing an emphasis on reusability and conciseness.
If you are not familiar with how to use hooks, please read this page.
Though with amazing power, if not careful, you could end up causing trouble.
Hooks provide several benefits to a functional component — less code, functional programming, reusability. If not careful, they do also introduce a few gotchas you must look out for.
In this article we will touch upon four topics:
Follow the Rules of Hooks
Hook Dependencies
useRef vs useState
State Objects with useState
As the Rules Of Hooks states: Don’t call Hooks inside loops, conditions, or nested functions.
What this means is that they must be top-level. A couple of illegal examples:
const list = ['1', '2', '3']
list.forEach((item: string) => {
const [itemState, setItemState] = useState(item) // ERROR
})if (condition) {
const [checked, setChecked] = useState(true)
// error
return <span>{checkedHook ? 'Checked' : 'Not Checked'}</span>
} else {
return <span>Hello</span>
}
Hooks may depend on props. These are called hook dependencies. Whenever a dependency changes, the hook is recalculated automatically. How does this work?
Let’s first look at an example of hooks that have dependencies. Currently in React we have: useEffect
, useCallback
, `useMemo`, `useLayoutEffect`, `useImperativeHandle`.
I’m guaranteeing you’ve used useEffect
to dispatch an action on first load that calls an API. For example, you want to load a set of users:
const MyComponent = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(loadUsers())
}, [dispatch])
return <span>hello</span>
}
Which is all fine and dandy. What if you need to, for some reason, notify the parent component when that effect dispatches? You might expose a prop:
const MyComponent = ({ startLoadingUsers }: Props) => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(loadUsers())
startLoadingUsers()
}, [dispatch, startLoadingUsers])
console.log('RENDER CHILD') // let's log our renders
return <span>hello</span>
}
Everything looks great. You expose it to the parent component as follows:
const ParentComponent = () => {
const [isLoading, setLoading] = useState(false)
const users = useSelector(selectUsers)
const loadUsers = () => {
setLoading(true)
}
console.log('RENDER PARENT') return <MyComponent startLoadingUsers={loadUsers} />
}
What you might not expect is as follows:
The log goes infinitely and eventually will crash chrome when it runs out of memory.
What is happening? Let’s break it down:
useEffect(() => {
dispatch(loadUsers())
startLoadingUsers()
}, [dispatch, startLoadingUsers])
We’re passing a function reference for startLoadingUsers
. If that reference changes, we recompute the effect here.
2. Our parent component constructs the lambda on every render:
const loadUsers = () => {
setLoading(true)
}
3. Parent depends on a state piece that changes by the child component directly:
const users = useSelector(selectUsers)
This will re-render when the users state changes, causing #2 to be recomputed, passing it down to #1, causing a new network call to load users, which then in turn calls the selector here again infinitely in a loop.
To fix this, wrap loadUsers
in useCallback
in the Parent component
const loadUsers = useCallback(() => {
setLoading(true)
}, [setLoading])
This ensures only one instance gets passed down. This is not ideal as every parent component to our child component must remember to memoize the functions.
Good Practice 101: Always wrap functions declared inside your functional component with useCallback
. This will ensure efficiency.
useRef
is used to easily capture a value so we can perform an action on them as needed. For example, we can use them to focus an input element:
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
This is great, however there is a gotcha. When the reference is set, useRef
does not trigger a re-render in your component. Where this may be useful is when we want to grab the clientHeight
of a specific component and adjust the UI based on it:
const bottomContainerRef = useRef<HTMLDivElement>(null)<HorizontalSpacer
background={COLORS.PaleGrey}
height={(bottomContainerRef && bottomContainerRef.clientHeight) || 0} />
This will always be set to 0 as when the first render pass happens, we do not have a ref quite yet.
To fix this, we need to change the useRef
to a useState
and then the UI will re-render as expected!
const [bottomContainerRef, setBottomContainerRef] = useState<HTMLDivElement | null>(null)
useState
has opened doors to function components that were only available to class components. We typically had a state object as follows:
state: State = {
userName: '',
password: '',
}
When we want to update the field input for userName
we do:
const updateUserName = (value: string) => setState({ userName: value })
This version of setState
diffs the object tree and re-renders the component.
When we useState
:
const [{ userName, password }, setState] = useState({
userName: '',
password: '',
})const updateUserName = useCallback((value: string) => {
setState({ userName: value })
}, [setState])
If state is currently:
userName: ‘Andrew’
password: ‘fluff’
Then we call updateUserName('Andy')
, our state results in:
userName: 'Andy'
password: undefined
In fact, in Typescript this would not even compile. What happens here is that useState
does a pure swap and not a diff. One potential solution is to pass the existing object before modifying it:
const updateUserName = useCallback((value: string) => {
setState({ ...state, userName: value })
}, [setState, state])
“With great power comes great responsibility”
Know your hooks and watch out for gotchas! They are a double-edged sword in that while they provide significant convenience, power, and responsibility rolled up into a great package, they also have internal complexities and gotchas you need to still be aware of.
Consult the online documentation on hooks. Ensure you are using them with performance in mind. When in doubt, console.log!
I hope you enjoyed this post.