React hooks are a great addition to React which has completely changed how we write code. Hooks were introduced in React starting with version 16.8.0.
Before React hooks, there was no way of using state and lifecycle methods in functional components, and that’s why the functional components were called Stateless Functional Components
.
Now, with the introduction of React hooks, we can use life cycle methods and state in functional components.
Using hooks makes our code a lot simpler, shorter, and easy to understand.
Installation
To start using React hooks, you need to use the latest version of create-react-app
. If you are using your own webpack setup for React as described in this article, you need to install the latest version of react
and react-dom
using
npm install react@latest react-dom@latest
Let’s start exploring React Hooks now.
Hooks are the functions that let you “hook into” the React state and lifecycle features in functional components.
React provides many built-in hooks and also allows us to create our own custom hooks.
Let’s look into some of the most important built-in hooks.
useState Hook
This is the first hook provided by React which allows us to use state inside the functional component.
The useState
hook accepts a parameter which is the initial value of the state.
In class-based components, state
is always an object but when using useState
, you can provide any value as the initial value like number
, string
, boolean
, object
, array
, null
etc.
The useState
hook returns an array whose first value is the current value of the state and second value is the function which we will use to update the state similar to setState
method.
Let’s take an example of a class-based component which uses state and we will convert it into a functional component using hooks.
Demo: https://codesandbox.io/s/delicate-thunder-xdpri
Let’s convert the above code to use hooks.
Demo: https://codesandbox.io/s/elegant-heyrovsky-3qco5
Let’s understand the above code
1. To use the useState
hook, we need to import it as we have done it in the first line.
2. Inside the App component, we are calling useState
by passing 0 as the initial value and using destructuring syntax, we stored the array values returned by useState
intocounter
andsetCounter
variables.
3. It’s a common convention to prefix the function name used to update the state with set
keyword as in setCounter
.
4. When we click the increment button, we are defining an inline function and calling the setCounter
function by passing the updated counter
value.
5. Note that, as we already have the counter
value, we have used that to increment the counter using setCounter(counter + 1)
6. Since there is a single statement in the inline on click handler, there is no need for moving the code into a separate function. Though you can do that if the code inside the handler becomes complex.
We can have multiple useState
calls in a single functional component:
Demo: https://codesandbox.io/s/long-framework-n1tyn
As you can see in the above code, we have declared two useState
calls and both username and counter are independent of each other.
We can add as many useState
calls as needed.
The state is not merged when using the useState hook:
One thing to note if you are using object
in useState
is that the state is not merged when updating the state.
Let’s see what happens when we combine the username
and counter
into an object to update the state.
Demo: https://codesandbox.io/s/sleepy-darkness-6jz9b
If you run the code and try to increment the counter, it works but as soon as you try to type into the input field, the counter value vanishes and when you again try to increment the counter, it shows as NaN
and you will also get a warning in the console.
This is because using an object in useState
will completely replace the object instead of merging it when we set state using setState
method.
In the above code, the initial state value is
{ counter: 0, username: "" }
but when we call increment
method, it becomes
{ counter: 1 }
and so username
is lost.
If you type in the input field first, then
{ counter: 0, username: "" }
becomes
{ username: "new_value" }
and so counter
is lost.
Because of this, it’s recommended to use multiple
useState
calls instead of using a singleuseState
to store objects.
If you still want to use an object in useState
, you need to manually merge the previous state like this:
const handleOnClick = () => {
setState(prevState => ({
...prevState,
counter: prevState.counter + 1
}));
};
const handleOnChange = event => {
const value = event.target.value;
setState(prevState => ({
...prevState,
username: value
}));
};
Demo: https://codesandbox.io/s/strange-dawn-s0nyi
useEffect Hook
React provides a useEffect
hook using which we can implement lifecycle methods in functional components.
The useEffect
hook accepts a function as its first parameter and optional array as the second argument.
useEffect
lets you perform side effects in a functional component.
Data fetching, setting up a subscription, and manually changing the DOM in React components are all examples of side effects.
useEffect
takes two arguments: a function and an optional array.
useEffect
serves the same purpose as componentDidMount
, componentDidUpdate
, and componentWillUnmount
combined together.
Take a look at the below code:
Demo: https://codesandbox.io/s/stupefied-darwin-qpcmm
If you run the code, you will see that function provided in useEffect
is invoked immediately on page load i.e when the component is mounted. It will also be called when the component is re-rendered when any state or prop changes.
So when you click the increment button, the useEffect
hook will be called again.
So it serves the purpose of componentDidMount
and componentDidUpdate
lifecycle methods.
If you want the effect to be called only when the component is mounted, you need to provide an empty array as the second argument to useEffect
call.
useEffect(() => {
console.log("useEffect called");
}, []);
So now, this effect will be called only when the component is mounted and never again.
Demo: https://codesandbox.io/s/gifted-newton-71vzl
Take a look at the below code:
Demo: https://codesandbox.io/s/long-framework-n1tyn
As you can see, the useEffect
gets executed when the counter is changed and also when the username is changed.
If you want that to be executed only when the counter
is changed, you need to mention that in the dependencies array which is the second argument to useEffect
.
useEffect(()=>{
console.log("This will be executed when only counter is changed");
},[counter]);
This is very important to understand because if you are doing some computation in useEffect
when the counter is changed, there is no point in executing that code when the username or any other prop or state is changed.
So by providing the variable in dependencies array, the effect will be executed only when those variables are changed.
This is a great improvement because it will remove the need for the extra condition we used to have in the componentDidUpdate
method
componentDidUpdate(prevProps) {
if(prevProps.counter !== this.props.counter) {
// do something
}
}
So using useEffect
and mentioning dependencies removes the need of comparing previous props or state value with the current prop or state value because the code inside useEffect
will only be executed when the dependencies change.
If you have multiple dependencies, you can specify that as a comma-separated value in the dependencies array.
useEffect(()=>{
console.log("This will be executed when any of the dependency is changed");
},[counter, some_other_value]);
Cleaning up the effect
If you return a function from the useEffect
, it will be called before the component will be unmounted. So it’s similar to componentWillUnmount
method.
Let’s say you are using setInterval
function, then you can clear the interval in the function returned by useEffect
.
This cleanup effect runs when the component unmounts as well as before re-running the useEffect due to a subsequent render
Demo: https://codesandbox.io/s/sleepy-wood-biu89
In the above code, we have added scroll event handler which will be registered only once, when the component is mounted because we have passed an empty array in useEffect
dependencies list.
This code displays a message in the console when the page is scrolled.
When we click on the unmount component
button, the component will be removed and the function returned from the useEffect
will be called where we remove the scroll event handler and prints the message “component unmounted”
to the console.
It is important to note that in the child component, useEffect
will be called for every re-render of the parent component.
Take a look at the below code:
Complete code And Demo: https://codesandbox.io/s/jolly-tesla-gj170
Here, we are adding a new todo. So for every added todo, the TodoList component will be re-rendered and so the useEffect
defined in the TodoList will be called for every re-render printing the message in the console.
So if you don’t want to re-render the effect of TodoList, you need to pass an empty array []
as the dependency in the useEffect
second argument.
useEffect(() => {
console.log("This will be executed only once");
}, []);
Multiple useEffect in a single component:
You can also have multiple useEffect
calls in a single functional component, each doing different tasks, and React will call them in the order they are defined in the component.
useRef Hook
The useRef
hook allows us to get access to any DOM element which is same as working with ref
in React.
It has the following syntax:
const refContainer = useRef(initialValue);
The initialValue
is optional.
Demo: https://codesandbox.io/s/damp-water-fye8r
As you can see we have created the reference using
const usernameRef = useRef();
and assigned it to the input field. So now we can access the input field by using usernameRef.current
property anytime we want to get access to the input field.
useRef
is not exactly same as React ref
because 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.
Demo: https://codesandbox.io/s/black-cache-e5vlb
In the above code, we have created a timer that displays the updated time after every second. When we click the unmount component
button, then we are clearing the setInterval
thereby stopping the timer.
Also, note that we have added another useEffect
call which clears the timer after 5 seconds are passed.
The key point to remember is that we are able to access the same value of interval
variable in the first useEffect
, even when it’s assigned in the second useEffect
. If we didn’t use useRef
but used the normal variable, it will not be available in the first useEffect
because useEffect
creates a new function for every execution of the effect.
useReducer Hook
React provides a useReducer
hook which allows us to use redux for handling complex calculations without the need of installing the redux library.
So it helps to avoid the creation of boilerplate redux action, reducer code.
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.
The useReducer
has the following syntax:
const [state, dispatch] = useReducer(reducer, initialState);
The useReducer
accepts reducer
and initialState
and returns the current state
and a dispatch
function which we can use to dispatch
the actions.
Let’s understand through an example. We will create a todo list component that will allow us to add and remove a todo.
Let’s create a reducer
to handle the todo.
const todosReducer = (state, action) => {
switch (action.type) {
case "ADD_TODO":
return [...state, action.todo];
case "REMOVE_TODO":
return state.filter(todo => todo !== action.todo);
default:
return state;
}
};
We initialize the initial state of todosReducer
in initialState
ofuseReducer
as
const initialState = [];
const [state, dispatch] = useReducer(todosReducer, initialState);
and dispatch the actions like this:
dispatch({ type: "ADD_TODO", todo: value });
Complete Code and Demo: https://codesandbox.io/s/admiring-bose-jw6wx
As you can see, the useReducer
hook makes it very easy to manage redux state without actually using redux which is great improvement.
But this does not mean redux is useless now. If you are managing the larger state or have multiple reducers, you should use redux to handle that.
useReducer
just make it easy if you want to handle complex states in a single component.
Creating Custom Hooks
React allows us to create our own custom hooks using the built-in hooks. This allows us to extract component logic into reusable functions.
Let’s take the previous timer code from HERE and we will convert it into a custom reusable hook.
Demo: https://codesandbox.io/s/infallible-visvesvaraya-qkwdw
Here, we have encapsulated the code for displaying a timer in the useTimer
function and from that function we are returning the updated time.
So now any component that wants to display the time, can call the useTimer
function and use this timer hook.
It’s common convention to
begin
the custom hook name withuse
keyword likeuseTimer
.
Things to keep in mind when working with Hooks
- Only call Hooks at the top level. Don’t call Hooks inside loops, conditions, or nested functions. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple
useState
anduseEffect
calls. - Only call Hooks from React function components. Don’t call Hooks from regular JavaScript functions. But you can call it from your own custom Hooks.
That’s it for today. I hope you learned something new today.
I hope you've been enjoying my articles and tutorials I've been writing. If you found them useful, consider buying me a coffee! I would really appreciate it.
Don’t forget to subscribe to get my weekly newsletter with amazing tips, tricks, and articles directly in your inbox here.