React/ReactJS: Hooks

Hooks are introduced in React 16.8. Earlier, functional components do not have states inside them (they are called "stateless" components). Functional components are pure JavaScript functions that accept just props ("arguments") and return JSX. They do not have states, constructors and other lifecycle methods.

If your component is largely presentational, a functional component is what you need.

Now suppose you have a requirement to display the following simple statement:

The circumference of the circle of radius 1 is 6.283185307179586

It is just a simple statement. There is nothing to update, no props to display, no button to click. So you create a functional component called Circle() which returns the statement, enclosed between the paragraph element <p/>.

					
						import React from 'react';
						const Circle = () => {			
						    return (
						      <div>
						        <p>The circumference of the circle of radius 1 is 6.283185307179586</p>					
						      </div>
						    );
						}
						export default Circle;
					
				

And the above component displays just it.

Now suppose there is a change in requirement. That you have to make the radius value of the above statement dynamic, along with the computed circumference. There will be an additional input field to take the radius. You will have to update the below boxed parts of the statement.

The useState() Hook

This requirement actually calls for the use of a state variable. Which means, you have to convert your functional component into a class component. Quite a hassle. Especially when you have managed the program almost entirely without the use of a single state variable and have to do a makeover just for it. Well, cases as this is where Hooks come in handy. You can make use of the useState hook to achieve the same without converting your functional component to a class component. Meaning, you can use states in a functional component using the useState hook.

The first section of this tutorial is all about the use of useState hook. We will illustrate with an example.

A word of note however. Before you start off with Hooks in React, make sure the dependencies section in your package.json has the updated versions of react and react-dom, as versions earlier than 16.8.0 do not support Hooks.

						
						"dependencies": {
						  "react": "^16.8.0",
						  "react-dom": "^16.8.0",
						}
						
					

We first import useState from the react package. The useState declaration returns a pair of items: the first is the state (radius) and the second is the function to update it (setRadius()). The argument passed to useState() is the first value the state is assigned during initial render. Here we initialize the value of radius to 1, which is the value passed as an argument to useState()

						
							import React, { useState } from 'react';
							const Circle = () => {						 
							    const [radius, setRadius] = useState(1);
							    return (
							      <div>
							        <p>The circumference of the circle of radius {radius} is {2*Math.PI*radius}</p>
							        <input type='text' onChange={(el) => setRadius(el.target.value)}/>
							      </div>
							    );
							}
							export default Circle;
						
					

We input some value (radius) into the box. The change event is triggered inside which the setRadius() function updates the radius state.

We have thus achieved the workings of a state using the useState hook without changing the functional component to a class.

Hooks, however, cannot be used inside class components.

The useEffect() Hook

Now can we also emulate the lifecycle methods like componentDidMount, componentDidUpdate and componentWillUnmount? The useEffect hook serves this very purpose. If you intend to fetch some data from an API inside a functional component, you need to fetch it inside this hook. According to the official React documentation, useEffect "serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in React classes, but unified into a single API."

Below we fetch the details of some user in JSON format from https://jsonplaceholder.typicode.com/users/1 inside the useEffect() hook, update the state (w/ setUser()) and render it.

						
						import React, { useState, useEffect } from "react";
						const User = () => {
						  const [user, setUser] = useState([]);
						  useEffect(() => {
						  	fetch('https://jsonplaceholder.typicode.com/users/1')
							  .then(response => response.json())
							  .then(user => setUser(user))
						  });
						  return (
						  	<div>
						  		{user.name}
						  	</div>
						  );
						}
						export default User;
						
					

Now there is one unseemly behaviour you would have failed to notice here. Add console.log() with some message inside the useEffect() hook.

						
						import React, { useState, useEffect } from "react";
						const User = () => {
						  const [user, setUser] = useState([]);
						  useEffect(() => {
						  	console.log("mounting ... ");
						  	fetch('https://jsonplaceholder.typicode.com/users/1')
							  .then(response => response.json())
							  .then(user => setUser(user))
						  });
						  return (
						  	<div>
						  		{user.name}
						  	</div>
						  );
						}
						export default User;
						
					

Press F12 and check the console. You will notice that the message you just added is being logged indefinitely.

This is because we have missed passing the second argument to useEffect() which causes it to run every render. It runs, it fetches the data, it updates the state and causes the component to render. On re-rendering, the useEffect() is triggered again. And the cycle goes on. If you want this to be controlled, pass only those values as an array which need to be updated on change.

We will show this by setting another hook [book, setBook]. We pass this book state as a second argument to useEffect(). Everytime some text is entered into the input box, the onChange event is triggered thereby calling the setBook function which updates the book state with the new value entered into the input box.

						
						import React, { useState, useEffect } from "react";
						const User = () => {
						  const [user, setUser] = useState([]);
						  const [book, setBook] = useState("The Sirens of Titan");
						  useEffect(() => {
						  	console.log('mounting ...');
						  	fetch('https://jsonplaceholder.typicode.com/users/1')
							  .then(response => response.json())
							  .then(user => setUser(user))
						  }, [book]);
						  const updateBook = (e) => {
						    e.persist();
						    setBook(e.target.value)
						  }
						  return (
						  	<div>
						  		{user.name} is reading {book} 
						  		<br/>
						  		<input type="text" onChange={updateBook}/>
						  	</div>
						  );
						}
						export default User;
						
					

You will now notice that the message inside the useEffect() hook is logged only once after the first render. The useEffect() is called only for some change in the value book, which is passed as the second argument to useEffect().

So if you intend to stop any kind of rerendering of effect, you pass no values but an empty array [] as a second argument to useEffect(). This will make sure that it is run only once during component mount (and unmount), thereby preventing unwanted infinite loops.

						
						import React, { useState, useEffect } from "react";
						const User = () => {
						  const [user, setUser] = useState([]);
						  useEffect(() => {
						  	console.log("mounting ... ");
						  	fetch('https://jsonplaceholder.typicode.com/users/1')
							  .then(response => response.json())
							  .then(user => setUser(user))
						  }, []);
						  return (
						  	<div>
						  		{user.name}
						  	</div>
						  );
						}
						export default User;
						
					

Now how do we do the "cleanup" or achieve the purpose of componentWillUnmount()? We need to cancel API requests, invalidate timers, etc. This is achieved by passing a function to the return statement. You can use the AbortController interface to abort your fetch requests.

						
						  useEffect(() => {
						  	const abortController = new AbortController();
						  	return () => {
						  		abortController.abort();
						  	};
						  }, []);
						
					

Now that we can use states and even the lifecycle methods inside a non-class component; will this make class components in React redundant?

The answer so far is No. The React team confirmed it here.

The useContext() Hook

We first do a brief recap of the Context API. Traditionally, data from a parent component to its child is passed via props at every level. Using createContext() we avoid doing that. In the example below, we pass nothing from the parent component <Irrational/> to its child component <Pi/> explicitly, yet the value of pi is accessible inside <Pi/> because of Context.

						
						import React from "react";
						const PiContext = React.createContext();
						const Irrational = () => {
						  return (
						    <PiContext.Provider value={3.141}>
						      <div>
						        <Pi/>
						      </div>
						    </PiContext.Provider>
						  );
						}
						const Pi = () => {
						  return (
						    <PiContext.Consumer>
						      {(value) => <div>The value of pi is {value}.</div>}
						    </PiContext.Consumer>
						  );
						}
						export default Irrational;
						
					

We can do away with the Consumer component by passing the desired context object to the useContext() hook.

						
						import React, { useContext } from "react";
						const PiContext = React.createContext();
						const Irrational = () => {
						  return (
						    <PiContext.Provider value={3.141}>
						      <div>
						        <Pi/>
						      </div>
						    </PiContext.Provider>
						  );
						}
						const Pi = () => {
						  const pi = useContext(PiContext);
						  return (
						      <div>The value of pi is {pi}.</div>
						  );
						}
						export default Irrational;
						
					

Besides the above basic hooks, React provides some more additional built-in Hooks: useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect, useDebugValue.

Notes

  • Hooks do not work inside class components.
  • A list of APIs for the built-in Hooks is given here.