17
Learning React Hooks
Are you planning on learning React Hooks next?
I did all the homework.
Follow me on Twitter 101.2K where I post all things JavaScript
Before moving with this tutorial please consider supporting my work.
Hey guys check out My Coding Books ( CSS, JavaScript and Python.) octopack
Support my free tutorials Get Octopack Discount on coding books.
Discounts Applied
for my Hashnode readers only!
Many hooks tutorials (including official docs) show syntax and how hooks work. But they don't mention setbacks you're 100% guaranteed to run into.
For example useState({})
hook doesn't merge state automatically.
I wrote this React Hooks tutorial when I was still learning them myself. I documented common quirks and pitfalls you'll run into and how to solve them. Hopefully this will help any new React learners avoid them.
If you've never had any previous experience with React Hooks setState()
and setEffect()
are the two hooks you want to learn first.
In this React Hooks tutorial we'll explore them before diving into other, more complex hooks, like useContext()
, useRef()
and useReducer()
setState()
imitate class-based state in function components.
setEffect()
imitate multiple lifecycle methods with just 1 function.
useContext()
with </Context.Provider>
and </Context.Consumer>
useRef()
one use case is to grab some instances of elements from DOM.
useReducer()
to be used with a reducer function.
useImperativeHandle()
while useRef()
gives instance of component ref is attached to this is similar. But it also gives you control over return value. It will allow you to replace native events like blur, focus, mousemove etc. with your own functions to run side effects on or rewrite their behavior.
useLayoutEffect()
similar to useEffect()
but for post-update side effects. Occurs after all DOM mutations. This avoids jumpy behavior when dealing with things like calculating properties that deal with element's dimensions like top, left, width and height.
useDebugValue()
Called only when React DevTools are open and related hook is inspected. In some cases this can prevent low performance when you need to narrow down on a specific value or property.
Creating your own custom hooks. Yes you can create your own hooks!
Oh. They're not exactly better. Just simpler. Wait – simpler is better!
Hooks are not a superset of React functionality.
For the most part they don't offer any new functionality.
So what are some key points then?
Hooks are just a way to 'hook' into existing features of React from your function-based components (as opposed to classes.)
But your code becomes cleaner, less repetitive and easier to maintain.
Hooks still work exactly the same way you'd expect React to work.
Eventually you might want to switch all React code to function components.
This will avoid dealing with updating data in large hierarchies of classes.
Which can overcomplicate your UI and make it difficult to maintain.
But you shouldn't have to rewrite your existing class based components.
You can mix them. Unless you were actually planning on going 100% hooks.
Simply add hooks into function-based components when you need them.
You can think of hooks as the next evolutionary step of React syntax.
It's ok to mix them with your older class-based components code.
Just know that hooks cannot be used inside class based components.
For example useState
hook provides a way for your functional component to create and access state data without having to specify it in class constructors.
To start using useState()
first you have to import it from react:
import { useState } from "react";
Here's minimum code for creating a single state variable: number
function App(props) {
// This is the state hook - add a state variable number:
const [number, setNumber] = useState(2);
return (<div>Number: {number}</div>);
}
useState
takes a default value.
Default value can be number, string, array or object.
Once rendered this component will display Number: 2
By using useState()
hook you are "hooking" into React's state functionality without having to define state in a class constructor. However the syntax is much simpler and that makes your code look better and easier to read and write.
If you ever worked with class-based state you often used this.setState({})
method to set state data which will trigger automatic component update.
If our previous example used class-based component in order to change the value of the state variable number you would do something like:
this.setState({ number: 2 })
This code could have been added to a button click or some other event.
But here is the important part:
Note that this.setState will merge { number: 2 }
with any other data present in the class-based component's state object. useState
hook does not! useState
will replace the entire value with the new data. We'll take a look at this later on.
In contrast useState()
hook requires to provide value AND setter function. In first example above value name wasnumber and setNumber
is its setter.
So for example if you want to change the value of number you no longer have to use the this.setState({ number: 1000 })
method from class-based components. Instead you will use setNumber
:
function App(props) {
// Add a state variable number:
const [number, setNumber] = useState(0);
// Event function for increasing number by 1
const inc = () => {
// use the setNumber setter function
setNumber(number + 1)
}
return (<button onClick = {inc}>{number}</button>);
}
Clicking on this button will increase its count by 1
Note we no longer need to use this like in class-based components.
In class-based components you would have a massive constructor and generally slightly messier-looking code to do exactly the same thing.
Naturally useState()
supports all common JavaScript data structures.
After working with useState()
for a while you'll find yourself doing stuff like:
const [number, setNumber] = useState(0);
const [loading, setLoading] = useState(true);
const [title, setTitle] = useState("Title");
const [movies, setMovies] = useState(["Alien", "King Kong"]);
const [data, setData] = useState({key: "skeleton"});
So should you just add another useState()
for each separate value?
You don't have to but…
…with time you will eventually think about bundling related data in objects.
To store multiple values with one useState() hook just use a single object:
function Cat(props) {
const [state, setState] = useState({
name: "Luna",
age: 2,
legs: 4,
state: "Sleeping",
})
return (<div>
Render cat {state.name} with {state.legs} legs.
</div>)
}
Now this looks similar to class-based constructors. Doesn't it?
Now every time you need to update your cat use setState({ legs: 5 })
Our previous example demonstrated how to update a single state property.
You'll notice that it's really not a problem when it comes to single values.
But changing more than one property requires a manual merge.
This is relevant only when using useState
with objects or arrays.
There's important distinction between useState()
hook and the older way of updating state using class-based this.setState()
function when it comes to updating more complex data structures like arrays and objects.
And it's to do with updating single entry in a larger data structure where multiple values or properties are present. Like Array []
or Object {}
.
To demonstrate this problem let's take a look at the following example.
Using useState()
hook with objects {}
or arrays []
Changing entire objects or arrays isn't exactly the same as primitive values.
Using useState()
hooks with an object:
function Component() {
let [state, setState] = useState({
name: "Luna",
age: 2,
legs: 4,
state: "Sleeping"})
}
Let's use our setState()
function to change name from Luna to Felix
First let's define the function that will trigger the name update:
const changeName = () => {
setState({ name: "Felix" })
}
And now do it on a button click:
return <>
<h2>Hello Hooks</h2>
<button onClick = {changeName}>Change Name To "Felix"</button>
<div>
<div>Name: {state.name}</div>
<div>{state.legs} legs</div>
<div>{state.age} years old</div>
<div>{state.state}</div>
</div>
</>
Launching this app originally the output will be correct:
But clicking on the button will wipe out all other properties from state object.
Name will update correctly. But all other values become undefined
This is because our setState({ name:"Felix" })
setter function replaces the entire object with whatever we pass to it without merging it.
This might give you different vibes compared to the pre <= 16.8
React.
If you've used class-based this.setState({})
method you know that it will automatically merge whatever you pass to it with existing state data. However with this useState({})
hook this is not the case. You must merge it yourself before passing the new value to the setter function.
In class-based state things merge automatically
this.setState({ name: "Felix" })
In class-based React before hooks this would update name property in existing state and automatically merge it with the rest of the properties in the originally initialized state object.
setState
hook does not merge state automatically
With hooks this does not automatically happen. When you use useState() with complex data structure like array or object and you want to change just one entry in it you have to write an extra line of code to merge it.
Note: this is true only when dealing with {}
and []
data structures.
In order to tackle this merging problem you can use rest/spread operator.
This …
operator (actually it's a notation not an operator, but it's tempting to call it that) was added to JavaScript a while ago in EcmaScript 6:
let cat1 = { name: "Felix" }
let cat2 = { legs: 4 }
let merged = {...cat1, ...cat2 }
console.log( merged )
>>>
{name: "Felix", legs: 4}
But wait…sometimes our data is an array.
In the same way you can use …rest/spread operator to merge
let cat1 = ["Luna"]
let cat2 = ["Felix"]
let merged = [...cat1, ...cat2]
console.log( merged )
>>>
["Luna", "Felix"]
Now all you have to do is…
Finally to fix our original problem with hook's cat name state update let's update our changeName
function to support …
rest/spread notation.
If your state variable is an object you would do this - notice {}
const changeName = () => {
setState({...state, name: "Felix"})
}
And if your state variable was an array[]
you would do something like:
const changeArrayValue = () => {
setState([...state, "Felix"])
}
Now our function correctly updates the name and retains original state data:
This technique can be applied to updating a single value in any object that stores a set of multiple values. Just use the rest/spread notation!
It can be useful when filtering table data by rows.
Or when merging state with new data received from a fetch API request.
Or any time you need to update just one object property or array value.
Basically any time you need to update an object partially.
I assume you're already familiar with how lifecycle methods work in React.
The useEffect()
tells React to do something after rendering the component.
The useEffect()
hook can imitate multiple lifecycle events in one function!
This hook will behave similar to different lifecycle events based on how second argument is used: undefined, empty array[]
(has its own special meaning) or a list of state object dependencies [state1, state2, ...N]
)
The cool thing about useEffect
is that scheduled effects won't block your browser like lifecycle components would. This makes your UI even more fluid. And this is another good reason to start using hooks instead of class-based design.
To start using useEffect()
import it:
import { useEffect } from "react";
Place useEffect directly inside your function component:
function App() {
let [val, setVal] = useState(0)
let [num, setNum] = useState(0)
useEffect(() => {
// something happens here on a lifecycle event
})
return (<div>{val}</div>)
}
Note that all it takes is an arrow function. It will be executed whenever one of the lifecycle methods is triggered whenever any of the defined state objects change. In this case if either val or num change. This is default behavior.
So basically you can say if you skip second argument useEffect()
acts as a combination of 3 lifecycle methods: componentDidMount
, componentDidUpdate
and componentWillUnmount
.
Just as a reminder here's when they execute:
componentDidMount
is triggered when your component is mounted.
componentDidUpdate
is triggered right after component is rendered.
componentWillUnmount
is invoked when component is about to be removed from the DOM. Usually this is where you do data clean up.
setState
's behavior is defined by what you do with the second argument which is a dependencies[]
array. By default its undefined
useEffects(effect, dependencies[])
The effect function handles your side effects.
dependencies[]
is optional for simple use case. But its key to understanding and taking full advantage of useEffects
Based on whether it's present (or not) and also on what state objects are passed as dependencies[]
you can narrow down not only on which lifecycle methods you want useEffect
to be triggered for but also choose which particular state objects you want this effect to trigger on for future updates.
This isn't just about lifecycle events.
It's also about filtering which state objects you want to execute effects on.
This is explained in the following examples.
So pay close attention 🙂
With undefined dependencies[]
array:
By default if you skip optional dependencies[]
completely your effect on this component will update in at least two default cases:
- After first render and,
- Every time any state is updated again.
Important Note: In this case the effect will be triggered on component for any and all of its state objects. Not just one state object.
We've already implemented that in our first useEffect()
example.
Next use case is when the dependencies[]
array exists but it is empty []
.
This is not the same as default effect behavior.
With empty []
array effect is executed only once for the first time
This disables any future updates for all state objects.
Basically it's like saying: execute this effect after component was rendered for the first time only. And don't do any future updates even if any of state objects change (though that's less of a point here.)
function App() {
let [val, setVal] = useState(0)
useEffect(() => {
// same as componentDidUpdate -- but fires only once!
}, [])
return (<div>{val}</div>)
}
Note here we added empty array []
as second argument of useEffect
This means arrow function will be triggered only once when component is rendered for the first time. Which has its use. But it is a narrow use case.
If you want the effect function to be also triggered every time state of this component is updated in the future you can also pass it as a dependency:
function App() {
let [val, setVal] = useState(0)
let [num, setNum] = useState(100)
let [txt, setTxt] = useState('text')
useEffect(() => {
// same as componentDidUpdate
// AND fires in the future only for val changes
// nothing happens if either num or txt change
}, [val])
return (<div>{val}</div>)
}
In this example we have val
, num
and txt
state variables.
We only added [val]
in dependencies[]
array.
Now useEffect()
will trigger on mount and whenever val is updated.
Remember when dependencies[]
is missing it's like executing useEffect
whenever any of the state variables defined with useState
change.
But because we listed [val]
then useEffect
excludes all other other state variables and will be executed only when val
changes. If any other state objects change useEffect
will not be executed for them.
You can think of dependencies[]
array as a filter.
Here's another example that will be executed only on val and num changes:
useEffect(() => {
// also executes in the future for val and num
// but not for txt
}, [val, num])
But if txt
changes this effect will not execute.
It does take playing around with this a bit in order to fully sink in.
Context is used with providers. Honestly I haven't used either provider nor consumer pattern a lot in my code. But if you are familiar with them here's how you would implement them in a functional component:
const action = {
learning: 1,
working: 2,
sleeping: 3
}
const ActionContext = createContext(action)
function App(props) {
return (
<ActionContext.Provider value={action.sleeping}>
<ActionPicture />
</ActionContext.Provider>
)
}
ActionContext
is the provider that provides the value action.
function ActionPicture() {
const action = useContext(ActionContext);
return <div>{ action }</div>
}
// consumer component
function ActionPicture() {
return <ActionContext.Consumer>{
({ action }) => <div>{ action }</div>
}</ActionContext.Consumer>
}
This will require some basic knowledge of how refs work in React.
You need to import useRef from react package to start using it:
import { useRef } from 'react';
Basically it's about two things:
- Mutable Values.
- Accessing DOM Elements.
Refs automatically create .current
property on the ref
(.current
can point to a regular variable or link to a DOM object which depends on how you initialized your ref
and where it's used.)
Not all data requires a state update. Especially DOM element properties.
Use useRef(initialValue)
to create persisting mutable values.
To keep track of a value without triggering a screen update
Use useRef(initialValue)
if you need to get instance of DOM element.
To focus on input field when component mounts, for example.
Changing the value of a ref will not trigger an update.
It's like state except it's decoupled from rendering process.
// value is changed but nothing happens when button is clicked
function App() {
const count = useRef(0)
return (<button onClick={() => count.current++}>
{count.current}
</button>);
}
The count.current
value will change but on the screen it remains at 0
even if button is clicked multiple times. Changing it won't trigger a redraw.
Use useRef if you want to grab an element from DOM.
// use useRef if you want to grab element from DOM
function App() {
const butt = useRef(null)
const clickIt = () => butt.current.click()
return (<button ref={butt}></button>)
}
Here the button click is actually executed by calling native .click()
method.
butt.current
is the link to the button's element in the DOM.
Another use case is focusing on an input element when component mounts.
Let's create a search component for entering a search query.
Because search query is single most important input element on so many apps and websites page often automatically focuses on it once it's loaded:
import { useRef, useEffect } from 'react';
function SearchQueryInput() {
const queryRef = useRef()
// Note: queryRef is still 'undefined' here
useEffect(() => {
// But here queryRef becomes a
// valid HTMLInputElement
queryRef.current.focus()
})
return (
<input
ref = {queryRef}
type = "text"
/>
);
}
First we create our
queryRef
withuseRef()
, this will hold an object reference to the input element (which will be pointed to byqueryRef.current
property notqueryRef
itself.)When
useEffect
executes on this component (which will happen soon after first render) we call.focus()
method onqueryRef.current object
. This automatically gives our search query input field typing focus.The return value which is just the
<input>
element is linked toqueryRef
object via the ref attribute. It's assigned to{queryRef}
which is the variable name we assigned to result returned fromuseRef()
hook.Note that initially
queryRef
is still undefined soon as its created. It becomes available only inuseEffect
after component is mounted.
Running this code will produce an automatically focused search query input. But of course you can call any other of the DOM methods on the input object.
This hook helps with performance optimizations. This becomes important when you have some expensive calculation that your React component needs to perform. You can think of it as a cache for complex computations.
The idea is simple.
If you run a pure function with same arguments it always produces the same return value. By definition that's what a pure function is.
So why execute the same calculations again if we already know what a function will return from just knowing the combination of its arguments?
Memoization creates a list of function's return values. You can think of it as caching function return values. Every time a memoized function is executed React first looks at this cache to see if it has already been executed with those same arguments. If so it returns the cached return value. This way you avoid unnecessary repetitive calculations and improve performance.
import { useRef } from 'react';
Let's memoize a function:
const memoized = useMemo(() => sum(a, b), [a, b]);
To memoize a function wrap it in useMemo()
[a, b]
is the dependency array. It should contain all values referenced inside the function. This is what potentially improves performance.
React memorizes result of the function when same values are passed.
This way instead of going through calculations in the function body again React gives you the value already stored (if available) in results table produced when function was previously executed with same arguments.
Function inside useMemo
will run during component rendering. Avoid doing anything here that would trigger a re-render (like change state.) Side effects should go in useEffect
hook.
Make sure your code runs as intended without memoization. And only then apply useMemo
. React doesn't always guarantee its execution. But it does provide additional optimizations when it makes the most sense.
Implementing useMemo too often can undermine performance.
Do not useMemo(myFunction, [arg])
this won't work.
Instead return it from an arrow function:
useMemo(() => myFunction(), [arg])
It's important to use an arrow function here to memoize your function.
With useMemo()
we can return memoized values and avoid re-rendering. This works as long as the function's arguments have not changed.
I don't know yet whether useMemo
should be used to memoize entire components or how exactly to do it. So (if that's even possible) I'll work on this and included it in this section later.
But I do know you can use React.memo()
method to achieve that. (Even though it's not really part of React hooks.)
This is not entirely the same as useReact hook. But the idea is the same.
You can use React.memo to wrap around your function-based components.
// Song.js
export function Song({ title, singer, year }) {
return(
<div>
<div>Song title: {title}</div>
<div>Singer: {band}</div>
<div>Release year: {year}</div>
</div>
)
}
// Export Song as memoized component
export const MemoizedSong = React.memo(Song);
Then import this component and render somewhere in your code:
<MemoizedSong
title="Lose Yourself"
singer="Eminem"
year="2002"
/>
When this component is rendered for the first time memoization will occur and store its result in some hidden cache object under the hood.
Next time this component renders React will look at its memo cache, check if component is rendered using same arguments, and if the cached result for this component with matching arguments exists it will return that value.
This creates a performance improvement because React won't call render on memoized components.
By default React.memo()
does a shallow comparison. This means only first-level properties will be compared without checking full hierarchy of objects.
This isn't always what you want.
You can also compare props using areEqual function:
React.memo(Component, [ areEqual(prevProps, nextProps) ]);
The areEqual
function returns true if previous and next props are the same.
Pure functional component. Your <Component>
is functional and given the same props so it always renders the same output.
Frequent renders. Your component is rendered frequently.
Re-renders with the same props. Your <Component>
is always (or often) rendered with the same props.
Medium to big size components. Your <Component>
contains a decent amount of UI elements.
If component usually renders with different props all the time. No performance benefit here. Your memo cache will continue to grow without much reuse. This can actually make your UI slower.
React.memo()
can cause performance issues if not correctly implemented.
The purpose is to memoize callbacks.
Like other hooks useCallback takes an arrow function as its first argument.
import { useCallback } from 'react';
The useCallback
hook is used with callback functions. This basically memoizes callback functions making them efficient.
But be careful. Memoizing all callbacks all the time can actually reduce performance in some cases. Like with other hooks, it's important to use them correctly with their intended purpose.
Here's a basic example of how to use useCallback:
function ClickMe() {
const doClick = useCallback(() => {
// handle click
}, [])
return (<button onClick = {doClick}>Click Me</button>)
}
Like useMemo
hook useCallback
will memoize callbacks.
In order to do that React must compare this callback with previous callback.
If you look at simple ClickMe
component below notice doClick
function:
function ClickMe() {
const doClick = () => {
console.log('Button Clicked!')
}
}
Well every time you render this component a new doClick
function is created. Inline functions are inexpensive so a new object is created.
That's fine in most cases but there are times when you need to retain the same function object between multiple renderings.
Functional components are sometimes wrapped inside React.memo()
. This function accepts props.
It can be used in dependencies of another hook like useEffect(effect, [callback])
function ClickMe() {
const doClick = useCallback(() => {
// handle click
}, [])
}
This means doClick
will always refer to same callback function. This can improve performance if used strategically in certain places in your app.
One classic use of useCallback
is when rendering long lists of components. Instead of having React assign a new callback function to each component the same function can be used.
This starts to matter if you have thousands of rows of data.
Don't forget to import useReducer first:
import { useReducer } from 'react';
Action -> Reducer -> Store -> Update UI -> Action
A reducer is a function that usually sits between some action and store update. This is why it's often used with redux. But you don't have to. It can be just your regular component state update.
To create a simple reducer on a state assign it to useReducer():
function App() {
const [state] = useReducer()
return (<>Count: {state}</>)
}
A reducer generally can be used to do some clean up or data preformatting when an API call returns from some CRUD action.
Here I'll use a basic example of a reducer function:
Example of reducer function:
function reducer(state, action) {
switch (action.type) {
case 'add':
return state + 1;
case 'subtract':
return state - 1;
case 'double':
return state * 2;
default:
throw new Error();
}
}
// returns an array of 2 values: state and dispatch
function App() {
// 2nd arg = initial state
const [state] = useReducer(reducer, 10)
return (<>Count: {state}</>)
}
return(<>
Count: {state}
<button onClick={() => dispatch({type: 'add'})}> + </button>
<button onClick={() => dispatch({type: 'subtract'})}> - </button>
<button onClick={() => dispatch({type: 'double'})}> X2 </button>
</>)
useReducer
takes reducer function and initial value (10 in this case).
useReducer
is usually used together with dispatch function.
The dispatch
function will often define action type as one of its arguments.
This action is then passed to a separate reducer function (reducer()
here.)
You can create your own custom hooks.
One of best ways to finally put an end to understanding how hooks actually work is to practice making your own hooks! React community is huge and chances are the hook you're thinking of making is already created by someone else on NPM.
Let's say you want to make your own completely custom hook.
But what should you name them? And what should they do?
Aren't existing react hooks like useState
and useEffect
enough?
A hook is simply a JavaScript function.
In fact it's a special type of a function called higher-order function.
A higher-order function takes another function as one of its arguments.
Your hook name should start with use*
Here's an example of a simple custom hook:
const useCustomHook = value => {
useEffect(() => {
console.log(`Do something, val = ${value}`);
}, []);
)
}
As you can see it's just an arrow function that takes an argument.
How you use this hook and in which situations is entirely up to you.
This is why they are custom hooks. Then you use them in your functional component as follows (this is just an implementation example.) It's not really doing anything actually useful:
function Something() {
const [count, setCount] = useState(0);
const inc = () => setCount(count + 1);
const value = `The count is ${count}`;
// Implement your hook
useCustomHook( value )
return(<div>
<h1>{count}</h1>
<button onClick = {inc}>Increase by 1</button>
</div>);
}
I would be careful about experimenting with my own hooks until a particular use case really sunk in. It really depends on what you're trying to accomplish.
Custom hooks can be designed around localStorage or some type of implementation scenario for storing data in arrays, for example.
A good purpose for hooks could be reducing the amount of repetitive code written to handle some commonplace pattern. They're kind of tiny plug ins that modify architecture of your functional components in React.
React Hooks are nothing different from original React features. They are simply a more succinct way to use the already familiar: state, lifecycle, context and refs. Hooks make React code cleaner! The useState effect simulates state of class-based components in function components. The useEffect hook minifies syntax of component lifecycle methods without sacrificing their function. Hooks are designed to work only in function-based components. You can't use hooks inside classes. However they can be still mixed with class-based components in a single tree.
You "hook" them to function components. The useEffect
hook, for example, inherits same functionality of lifecycle methods. But your code is cleaner. And it makes it easier to write same efficient code.
As of June 2021 if you apply for a React UI Engineer position you'll notice large majority of companies saying the same thing:
"Most of our React is still class based and we use lifecycle methods."
"But we're in the process of switching to hooks!"
This is understandable. So much React code is already written using lifecycle methods. Back in 2018 at my Texas coding interview I was asked about whether React is a framework or a library and also about lifecycle methods.
Most professional devs moved on to hooks…today interview can still be two-fold & you might be asked to code something using lifecycle methods which is fine (although it's becoming a lot more rare and if a company requires only that they probably don't know what they're doing.)
On the other hand there is generally a 99.99% chance you'll be asked about hooks. If you still don't know them it's best if you start learning now.
Even though hooks came out a long time ago in React 16.8 (Feb 16 2019) many companies are still in process of switching their React code to hooks.
Based on Twitter posts many developers who are already familiar with React are still considering learning hooks. And pretty much almost any new developer is likely to be tempted to skip lifecycle methods and learn hooks.
That means there is a good chance many developers are entertaining the idea of learning React Hooks. Hope this tutorial helped you make sense of it 🙂
It takes time to make tutorials for free! Please consider supporting my work.
Hey guys check out My Coding Books ( CSS, JavaScript and Python.) octopack
Support my free tutorials Get Octopack Discount on coding books.
Discounts Applied
for my Hashnode readers only!
17