Sep 14, 2021
Hooks introduce a new way to create stateful components in React. Since these components manage their internal states, component composition and testing become significantly easier.
Another exciting feature coming with React hooks is simplified lifecycle methods. As React hooks can be used multiple times in a component, we can implement independent code blocks to handle separate concerns.
In this guide, we will go over the most common React hooks and their use cases.
Before exploring React hooks, let's review two main rules of hooks:
These rules are enforced by some implementation details, luckily they are easy to remember.
useState
hook converts plain JavaScript functions into stateful React components with the following signature:
const [<state value>, <state setter>] = useState(<initial value>)
We created a cool library list in React for Beginners guide. CoolLibrary
has a state named likes
for the number of likes. Let's add another state for the number of unlikes.
function CoolLibrary({ name }) {// create local states for number of likes and unlikesconst [likes, setLikes] = React.useState(0);const [unlikes, setUnlikes] = React.useState(0);// handle button click for likefunction handleLikeClick() {setLikes(likes + 1);}// handle button click for unlikefunction handleUnlikeClick() {setUnlikes(unlikes + 1);}return (<p>{name} has {likes} likes and {unlikes} unlikes!{" "}<button onClick={handleLikeClick}>Like</button>{" "}<button onClick={handleUnlikeClick}>Unlike</button></p>);}function LibraryList({ libraries }) {return (<div>{libraries.map((library) => (<CoolLibrary name={library} />))}</div>);}ReactDOM.render(<LibraryListlibraries={["React", "Vue.js", "Angular", "Svelte", "Ember.js"]}/>,document.getElementById("root"));
As we can see from the sample code, each useState
call creates an independent state variable and its setter function. We can use setLikes
and setUnlikes
to update likes
and unlikes
respectively.
useEffect
hook performs side effects with the following signature:
useEffect(<callback>, <optional dependencies>);
Defining side effects can be a little ambiguous, so let's learn useEffect
through examples.
Until now, our CoolLibrary
component displayed only local states (likes
and unlikes
). What would happen if we wanted to show GitHub stars for each library? We would need to get the data from GitHub API.
function CoolLibrary({ name, repository }) {// create local statesconst [likes, setLikes] = React.useState(0);const [unlikes, setUnlikes] = React.useState(0);const [stars, setStars] = React.useState(0);// handle button click for likefunction handleLikeClick() {setLikes(likes + 1);}// handle button click for unlikefunction handleUnlikeClick() {setUnlikes(unlikes + 1);}React.useEffect(() => {// fetch the number of starsconsole.log("fetching data for " + name);fetch("https://api.github.com/repos/" + repository).then((response) => response.json()).then((data) => {// check the console for the logsconsole.log("fetched data for " + name);setStars(data.stargazers_count);});});return (<p>{name} has {likes} likes, {unlikes} unlikes and {stars} stars!{" "}<button onClick={handleLikeClick}>Like</button>{" "}<button onClick={handleUnlikeClick}>Unlike</button></p>);}function LibraryList({ libraries }) {return (<div>{libraries.map(({ name, repository }) => (<CoolLibrary name={name} repository={repository} />))}</div>);}ReactDOM.render(<LibraryListlibraries={[{ name: "React", repository: "facebook/react" },{ name: "Vue.js", repository: "vuejs/vue" },{ name: "Angular", repository: "angular/angular" },{ name: "Svelte", repository: "sveltejs/svelte" },{ name: "Ember.js", repository: "emberjs/ember.js" }]}/>,document.getElementById("root"));
By default, useEffect
's callback is called after every render, so if any prop or state changes, the number of stars will be fetched again. Even though likes
and unlikes
are unrelated to the number of stars, a change in their values triggers useEffect
. Check the console outputs on CodePen for more!
In order to prevent the extra fetch
calls, we can pass an empty dependency array to useEffect
.
function CoolLibrary({ name, repository }) {// create local statesconst [likes, setLikes] = React.useState(0);const [unlikes, setUnlikes] = React.useState(0);const [stars, setStars] = React.useState(0);// handle button click for likefunction handleLikeClick() {setLikes(likes + 1);}// handle button click for unlikefunction handleUnlikeClick() {setUnlikes(unlikes + 1);}// `useEffect` hook with an empty dependency arrayReact.useEffect(() => {// fetch the number of starsconsole.log("fetching data for " + name);fetch("https://api.github.com/repos/" + repository).then((response) => response.json()).then((data) => {// check the console for the logsconsole.log("fetched data for " + name);setStars(data.stargazers_count);});}, []);return (<p>{name} has {likes} likes, {unlikes} unlikes and {stars} stars!{" "}<button onClick={handleLikeClick}>Like</button>{" "}<button onClick={handleUnlikeClick}>Unlike</button></p>);}function LibraryList({ libraries }) {return (<div>{libraries.map(({ name, repository }) => (<CoolLibrary name={name} repository={repository} />))}</div>);}ReactDOM.render(<LibraryListlibraries={[{ name: "React", repository: "facebook/react" },{ name: "Vue.js", repository: "vuejs/vue" },{ name: "Angular", repository: "angular/angular" },{ name: "Svelte", repository: "sveltejs/svelte" },{ name: "Ember.js", repository: "emberjs/ember.js" }]}/>,document.getElementById("root"));
As we can check from CodePen's console, clicking Like
and Unlike
buttons doesn't trigger useEffect
with an empty dependency array. It runs only once after the first render.
As useEffect
watches the changes in its dependencies, we can create a new state named points
and compute its value through likes
and unlikes
.
function CoolLibrary({ name, repository }) {// create local statesconst [likes, setLikes] = React.useState(0);const [unlikes, setUnlikes] = React.useState(0);const [stars, setStars] = React.useState(0);const [points, setPoints] = React.useState(0);// handle button click for likefunction handleLikeClick() {setLikes(likes + 1);}// handle button click for unlikefunction handleUnlikeClick() {setUnlikes(unlikes + 1);}// `useEffect` hook with an empty dependency arrayReact.useEffect(() => {// fetch the number of starsconsole.log("fetching data for " + name);fetch("https://api.github.com/repos/" + repository).then((response) => response.json()).then((data) => {// check the console for the logsconsole.log("fetched data for " + name);setStars(data.stargazers_count);});}, []);// `useEffect` hook depending on `likes` and `unlikes` statesReact.useEffect(() => {setPoints(likes - unlikes);}, [likes, unlikes]);return (<p>{name} has {points} points and {stars} stars!{" "}<button onClick={handleLikeClick}>Like</button>{" "}<button onClick={handleUnlikeClick}>Unlike</button></p>);}function LibraryList({ libraries }) {return (<div>{libraries.map(({ name, repository }) => (<CoolLibrary name={name} repository={repository} />))}</div>);}ReactDOM.render(<LibraryListlibraries={[{ name: "React", repository: "facebook/react" },{ name: "Vue.js", repository: "vuejs/vue" },{ name: "Angular", repository: "angular/angular" },{ name: "Svelte", repository: "sveltejs/svelte" },{ name: "Ember.js", repository: "emberjs/ember.js" }]}/>,document.getElementById("root"));
Whenever likes
or unlikes
changes, setPoints
will be called with the latest values.
useMemo
derives values from other variables with the following signature:
useMemo(<create function>, <dependencies>);
As you remember, we used useEffect
to compute points
through likes
and unlikes
in the useEffect section. useMemo
could be used for the same purpose.
function CoolLibrary({ name, repository }) {// create local statesconst [likes, setLikes] = React.useState(0);const [unlikes, setUnlikes] = React.useState(0);const [stars, setStars] = React.useState(0);// compute `points` through `likes` and `unlikes`const points = React.useMemo(() => likes - unlikes, [likes, unlikes]);// handle button click for likefunction handleLikeClick() {setLikes(likes + 1);}// handle button click for unlikefunction handleUnlikeClick() {setUnlikes(unlikes + 1);}// `useEffect` hook with an empty dependency arrayReact.useEffect(() => {// fetch the number of starsconsole.log("fetching data for " + name);fetch("https://api.github.com/repos/" + repository).then((response) => response.json()).then((data) => {// check the console for the logsconsole.log("fetched data for " + name);setStars(data.stargazers_count);});}, []);return (<p>{name} has {points} points and {stars} stars!{" "}<button onClick={handleLikeClick}>Like</button>{" "}<button onClick={handleUnlikeClick}>Unlike</button></p>);}function LibraryList({ libraries }) {return (<div>{libraries.map(({ name, repository }) => (<CoolLibrary name={name} repository={repository} />))}</div>);}ReactDOM.render(<LibraryListlibraries={[{ name: "React", repository: "facebook/react" },{ name: "Vue.js", repository: "vuejs/vue" },{ name: "Angular", repository: "angular/angular" },{ name: "Svelte", repository: "sveltejs/svelte" },{ name: "Ember.js", repository: "emberjs/ember.js" }]}/>,document.getElementById("root"));
Even if React re-renders the component, if the dependency values are the same, the create function won't be called again. Therefore, useMemo
is especially effective for time-consuming computations.
React allows converting stateful logic units into custom hooks. Creating custom React hooks reduces code duplication and simplifies testing. Any JavaScript function can be a custom hook, but custom React hooks start with use
prefix by convention.
In our CoolLibrary
component, we have two similar buttons increasing the number of likes and unlikes. Button clicks are handled by handleLikeClick
and handleUnlikeClick
functions. We can create useCounter
custom hook to count the button clicks. Here useCounter
manages its own state and reused for both like and unlike buttons.
// custom hook for likes and unlikesfunction useCounter() {// create a local stateconst [count, setCount] = React.useState(0);function increase() {setCount(count + 1);}// return `count` value and and `increase` functionreturn {count,increase};}function CoolLibrary({ name, repository }) {// call custom hookconst likeCounter = useCounter();const unlikeCounter = useCounter();// create a local stateconst [stars, setStars] = React.useState(0);// compute `points` through like and unlike countsconst points = React.useMemo(() => likeCounter.count - unlikeCounter.count, [likeCounter,unlikeCounter]);// `useEffect` hook with an empty dependency arrayReact.useEffect(() => {// fetch the number of starsconsole.log("fetching data for " + name);fetch("https://api.github.com/repos/" + repository).then((response) => response.json()).then((data) => {// check the console for the logsconsole.log("fetched data for " + name);setStars(data.stargazers_count);});}, []);return (<p>{name} has {points} points and {stars} stars!{" "}<button onClick={likeCounter.increase}>Like</button>{" "}<button onClick={unlikeCounter.increase}>Unlike</button></p>);}function LibraryList({ libraries }) {return (<div>{libraries.map(({ name, repository }) => (<CoolLibrary name={name} repository={repository} />))}</div>);}ReactDOM.render(<LibraryListlibraries={[{ name: "React", repository: "facebook/react" },{ name: "Vue.js", repository: "vuejs/vue" },{ name: "Angular", repository: "angular/angular" },{ name: "Svelte", repository: "sveltejs/svelte" },{ name: "Ember.js", repository: "emberjs/ember.js" }]}/>,document.getElementById("root"));
useCounter
is created for demonstration purposes, don't create custom hooks for all code duplications.Even though useState
, useEffect
and useMemo
are the most common hooks, React has other useful built-in hooks. We suggest skimming all hooks and spending more time when a specific hook is required.
We will publish more detailed guides on React, follow @crudfulcom and join Discord server for more!