All React Hooks Explained
Siddharth Roy
Posted on February 16, 2022
Since React 16.8 the most common way to build a react component is using function because now we can have all the features of class components in functional components using hooks.
But why use a functional component instead of a class-based component?
Using a functional component with hooks reduces the line of codes and makes our code look more clean and readable.
In this blog, you are going to learn how to use the most used built-in react hooks and how to make a custom hook from scratch.
useState
const [state, setState] = useState(initialState)
If you are used to class-based components you know that functional components don't state.
useState
allows you to have state in functional components.
// const [value, setValue] = useState(initialValue)
const [name, setName] = useState('Siddharth')
console.log(name) // => Siddharth
setName('React') // sets the name to "React" and re-render the component
// Value of name after re-render
console.log(name) // => React
The useState
hook is a function like every other hook. It takes an initial value
and returns an array containing the value
and a function to change the value
.
On first render the value
is set to initialValue.
The setValue function is for updating the value. It takes the new value as the first argument and triggers a re-render on the component.
Here is an example to understand it better:
import { useState } from 'react'
function App() {
console.log('Component render')
const [number, setNumber] = useState(32)
function updateNumber() {
setNumber(Math.random())
}
return (<>
<p>{ number }</p>
<br />
<button onClick={updateNumber}>Update number</button>
</>)
}
export default App;
NOTE: It's doesn't have to be named as value and setValue, you can name it anything you want but value and setValue style is preferred among all developers.
If the new value is based on the previous value then you can do this:
const [number, setNumber] = useState(0)
function updateNumber() {
// Do this
setNumber(prevNumber => prevNumber + 1)
// not this
setNumber(number + 1)
}
If you are storing an object inside a state then always use the object spread syntax to make a copy otherwise the component won't re-render.
const initialUserState = {
name: 'Siddharth Roy',
age: 17
}
const [user, setUser] = useState(initialUserState)
// Do this
setUser(prevState => {
let newState = prevState
newState.age = prevState.age + 1
return {...prevState, ...newState} // Make a new copy using spread syntax
})
// After re-render user.age is 18
// Not this
setUser(prevState => {
let newState = prevState
newState.age = prevState.age + 1
return newState
})
// Component won't re-render
The reason behind this is React uses Object.is
for comparing new value to previous value and if they are the same It won't re-render, and Object.is
does not check what's inside the object.
let obj1 = { name: 's' }
let obj2 = { name: 's' }
Object.is(obj1, obj2) // => false
obj2 = obj1
Object.is(obj1, obj2) // => true
// Using spread operator to copy the object
obj2 = { ...obj1 }
Object.is(obj1, obj2) // => false
NOTE: Spread operator won't copy nested objects, you will have to copy them manually.
useEffect
useEffect(didUpdate)
The useEffect
hook has many use cases, it is a combination of componentDidMount
, componentDidUpdate
, and componentWillUnmount
from Class Components.
Here is a simple demo of useEffect
hook:
import { useState, useEffect } from 'react'
function App() {
const [number, setNumber] = useState(0)
useEffect(() => {
console.log('This runs') // This will run when it mounts and update
})
return (<>
<p>{ number }</p>
<br />
<button onClick={() => setNumber(prevNum => prevNum + 1)}>Increase Number</button>
</>)
}
export default App;
The useEffect
hook is a function that takes a function as its first argument and that function will run when the component mounts and update
As you saw the function ran the first time when the component got mounted and whenever it updated.
This function in the first argument of useEffect
hook will only run when the component gets mounted and updated.
It also takes an array as a second optional argument and it behaves differently based on the array.
Like for this example, the function will run only run when the component mounts.
import { useState, useEffect } from 'react'
function App() {
const [number, setNumber] = useState(0)
useEffect(() => {
console.log('Component Mounted') // Only runs when the component gets mounted
}, []) // <-- Give an empty array in second argument
return (<>
<p>{ number }</p>
<br />
<button onClick={() => setNumber(prevNum => prevNum + 1)}>Increase Number</button>
</>)
}
export default App;
The array we passed on in the second argument is called dependency list, when we omit the list the function run when the component mounts and when the component update (eg. When a state change), when we put an empty array in the second argument it only runs when the component gets mounted.
You can also put state inside the dependencies list and it will only run when the component gets mounted and when the state changes.
import { useState, useEffect } from 'react'
function App() {
const [number, setNumber] = useState(0)
const [message, setMessage] = useState('Hi')
useEffect(() => {
console.log('Component Mounted') // Only runs when the component gets mounted
}, []) // <-- Give an empty array in second argument
useEffect(() => {
console.log('Component mounted or message changed')
}, [message])
useEffect(() => {
console.log('Component mounted or number changed')
}, [number])
return (<>
<p> { message} </p>
<p>{ number }</p>
<br />
<button onClick={() => setMessage(prevMsg => prevMsg + 'i')}>Increase Hi</button>
<button onClick={() => setNumber(prevNum => prevNum + 1)}>Increase Number</button>
</>)
}
export default App;
You can put multiple states in the dependency list but do note that if you are accessing any state from inside the function in useEffect
hook then you have to put that state in the dependencies list.
useEffect(() => {
// Do stuffs
}, [state1, state2, state3])
// Don't do this
useEffect(() => {
// Doing something with state1
}, []) // <= Not providing state1 in dependencies list
Now the last thing left is the cleanup function, this function is return by the function from the first argument and will run when the component gets unmounted.
useEffect(() => {
// Initiate a request to API and update a state
API.requestUserData()
return () => { // Cleanup function
// Cancel the request when the component gets unmounted
API.cancelUserDataRequest()
}
}, [])
Sometimes when we run an async function when the comp gets mounted if the function tries to update a state after the comp gets unmounted it can cause memory leaks so it's better to stop that from happening using the cleanup function.
useContext
const value = useContext(MyContext)
Normally if you want to share a state between components you would have to move the state to the uppermost component and then pass it down using props of every component. This method might be ok for small scale project but for a big scale project this can be tedious so to help with that useContext
allow you to have a global state accessible from any component without passing down the state.
There are two functions to note when using Context API
// Create a context with a default value
const context = createContext(defaultValue) // defaultValue is optional
const value = useContext(conext) // Get the value from context
Here is an example using Context API
In App.js
:
import { useState, createContext } from 'react'
import Component1 from './Component1'
import Component2 from './Component2'
import Adder from './Adder'
const Context = createContext()
function App() {
const [number, setNumber] = useState(0)
return (<Context.Provider value={{number, setNumber}}>
<p>Number: { number }</p>
{/* Any component inside this component can access the value of the context */}
{/* We can also provide the value of the context here */}
<Component1> {/* Dummy component */}
<Component2> {/* Dummy component */}
<Adder />
</Component2>
</Component1>
</Context.Provider>)
}
export { Context };
export default App;
In Adder.js
:
import { useContext } from 'react'
import { Context } from './App'
export default function Adder() {
const contextValue = useContext(Context)
return (<div style={{border: '1px solid black'}}>
<p>Inside Adder Component</p>
<p>Number: { contextValue.number }</p>
<button onClick={() => contextValue.setNumber(prevNum => prevNum + 1)}>Add Number</button>
</div>)
}
Explanation
- In
App.js
we are creating a context and using theProvider
Component inside theContext
object returned bycreateContext
as the uppermost component. Any component insideContext.Provider
Component can access the value of theContext
- We are also passing the
number
andsetNumber
fromApp.js
as the value of theContext
using the value prop of theContext.Provider
component - We need to export this
Context
object to be used inside the other components when usinguseContext
- In
Adder.js
we are simply importing theContext
object and using it withuseContext
hook to get the value of the context - The object returned by
useContext
contains the value we provided in the value prop of the provider component
Note that whenever the value of context change the entire component tree gets re-rendered and can affect performance. If you don't want that behavior it's better to use external solutions for global state management like react-redux
that only re-render the desired component.
You can also have multiple context and context providers if you want.
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init)
This is an alternative to useState
, it takes an additional function called reducer, it's similar to how redux handles state.
useReducer
is useful when you have a complex state, like an object with multiple sub-values.
Here is a simple counter example from React Docs using useReducer
:
import { useReducer } from 'react'
const initialState = {count: 0}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1}
case 'decrement':
return {count: state.count - 1}
default:
throw new Error()
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
)
}
export default App
Here is another example using complex state:
import { useReducer } from 'react'
const initialState = {
username: 'Siddharth_Roy12',
age: 17,
}
function reducer(state, action) {
switch (action.type) {
case 'increment_age':
return {...state, age: state.age + 1}
case 'decrement_age':
return {...state, age: state.age - 1}
case 'change_username':
return {...state, username: action.payload}
default:
throw new Error();
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
<p>Username: { state.username }</p>
<p>Age: { state.age }</p>
<button onClick={() => dispatch({type: 'decrement_age'})}>-</button>
<button onClick={() => dispatch({type: 'increment_age'})}>+</button>
<input
type="text"
value={state.username}
onChange={(e) => dispatch({
type: 'change_username',
payload: e.target.value
})}
/>
</>
)
}
export default App;
Lazy initialization
You can also create the initial state lazily. To do this, you can pass an init function as the third argument. The initial state will be set to init(initialArg)
.
It lets you extract the logic for calculating the initial state outside the reducer. This is also handy for resetting the state later in response to an action:
import { useReducer } from 'react'
const initialCount = 0
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1}
case 'decrement':
return {count: state.count - 1}
default:
throw new Error()
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialCount, init)
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
)
}
export default App
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
Usually, if you have an inline function in a react component, whenever that component re-render that function will also get re-created
The useCallback
hook takes an inline function and a dependencies list and returns a memoized version of that function. That function will only recreate when its dependencies change.
You can visualize the function re-creation using a Set
NOTE:
Set
can only have unique elements.Object.is
is used to check and remove duplicate elements.
Without useCallback
:
import { useState } from 'react'
const functionsCounter = new Set()
function App() {
const [count, setCount] = useState(0)
const [otherCounter, setOtherCounter] = useState(0)
const increment = () => {
setCount(count + 1)
}
const decrement = () => {
setCount(count - 1)
}
const incrementOtherCounter = () => {
setOtherCounter(otherCounter + 1)
}
functionsCounter.add(increment)
functionsCounter.add(decrement)
functionsCounter.add(incrementOtherCounter)
console.log(functionsCounter.size)
return (
<>
Count: {count}
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={incrementOtherCounter}>incrementOtherCounter</button>
</>
)
}
export default App;
With useCallback
:
import { useState, useCallback } from 'react'
const functionsCounter = new Set()
function App() {
const [count, setCount] = useState(0)
const [otherCounter, setOtherCounter] = useState(0)
const increment = useCallback(() => {
setCount(count + 1)
}, [count])
const decrement = useCallback(() => {
setCount(count - 1)
}, [count])
const incrementOtherCounter = useCallback(() => {
setOtherCounter(otherCounter + 1)
}, [otherCounter])
functionsCounter.add(increment)
functionsCounter.add(decrement)
functionsCounter.add(incrementOtherCounter)
console.log(functionsCounter.size)
return (
<>
Count: {count}
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={incrementOtherCounter}>incrementOtherCounter</button>
</>
)
}
export default App;
The use cases of the hook are very small, you will most likely never have to use this hook.
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
The useMemo
hooks take a function to compute a value and a dependency array and return a memoized value. This will only re-compute the value when its dependencies have changed.
This hook is useful when you are doing expensive calculations inside a component every time it renders.
An example without useMemo
:
function DemoComponent() {
const [state1, setState1] = useState(3)
const [state2, setState2] = useState(Math.PI)
const someValue = computeExpensiveValue(state1, state2) // Takes 0.6ms on every render
return (<>
{ someValue }
</>)
}
With useMemo
:
function DemoComponent() {
const [state1, setState1] = useState(3)
const [state2, setState2] = useState(Math.PI)
const someValue = useMemo(() => {
return computeExpensiveValue(state1, state2) // This only runs when the state1 or state2 changes
}, [state1, state2])
return (<>
{ someValue }
</>)
}
useRef
const refContainer = useRef(initialValue)
useRef
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.
The most common use case of this hook is to store a reference to a DOM 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>
</>
);
}
Another use case is to store a mutable value and it will persist during the entire life cycle of the component, but do note that whenever you change the .current
property the component won't re-render.
Custom hook from scratch
Now that you have learned how to use all react hooks It's time to build your own hook from scratch.
A custom hook is just a regular javascript function that uses the other hooks provided by React to extract component logic into a reusable function.
For example, look at this component
function App() {
const mounted = useRef(false)
useEffect(() => { // To check if component is mounted or not
mounted.current = true
return () => {
mounted.current = false
}
}, [])
// To check if the component is mounted or not check mounted.current
if (mounted.current) {
...
}
}
This component uses two hooks to check if the component is mounted or not. This is useful when you are running a long async function and the component can dismount at any time.
We can extract this logic into a reusable function.
function useIsMounted() { // React hook name must start from use
const mounted = useRef(false)
useEffect(() => {
mounted.current = true
return () => {
mounted.current = false
}
}, [])
return () => mounted.current
}
Then use it like this
function App() {
const isMounted = useIsMounted()
// To check if is mounted
if (isMounted()) {
...
}
}
Now our code looks more cleaner and we can use the same logic in many components.
Posted on February 16, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.