Table of Contents
Toggle- Required version of React: 16.8+
- What are React Hooks and why you should care about them – Part 1
Outline
- Context Hook
- Custom Hooks
- Important Rules with Hooks
- Sample application
In the first part of the blog, we discussed the main concepts of React Hooks and the reasons for using them. We compared the traditional ways of creating components and the new ways with hooks. In the second part, we will continue exploring the the different types of hooks, learn about the important rules and create a sample application. We have a lot to cover. Let’s get started!
Context Hook
What is a context?
First of all, what is a context in React?
Context is a way to share global data across components without passing props. Usually, data in React application is passed from parent to child through props. Sometimes we have some data that should be delivered to a component deep inside the component tree. It is tedious to manually pass the same data over and over again through all components. Instead, we can create a central store that can be inserted into any component, just like in Redux.
How about we see an example code without Context API and identify the need for using it.
Let’s say there is a banana plantation called “App” and it sets the prices for all bananas in the world. Before bananas reach the end customers, they need to go through wholesalers and supermarkets first. After that we can go to the stores and buy them. But since wholesalers and supermarkets need to add their profit margins in the process, the cost of the bananas go up.
Without Context API
// App.js
import React, { useState } from 'react';
const App = () => {
const [bananaPrice, setBananaPrice] = useState(2); // Original price: $2
return <Wholesaler price={bananaPrice}/>;
}
const Wholesaler = (props) => {
const price = props.price + 2;
return <Supermarket price={price}/>;
}
const Supermarket = (props) => {
const price = props.price + 3;
return <Customer price={price}/>;
}
const Customer = (props) => {
return (
<div>
<p>Plantation price: $2</p>
<p>Final price: ${props.price} 🙂</p>
</div>
);
}
What if we want to buy the bananas straight from the plantation and avoid the middlemen?
Now, the same code with Context API
With Context API
// App.js
import React, { useState } from 'react';
// First we need to create a Context
const BananaContext = React.createContext();
// Second we need to create a Provider Component
const BananaProvider = (props) => {
const [bananaPrice, setBananaPrice] = useState(2); // Original price: $2
return (
<BananaContext.Provider value={bananaPrice}>
{props.children}
</BananaContext.Provider>
);
}
const App = () => {
return ( // Wrap the component in Provider, just like in Redux
<BananaProvider>
<Wholesaler/>
</BananaProvider>
);
}
const Wholesaler = () => {
return <Supermarket/>;
}
const Supermarket = () => {
return <Customer/>;
}
const Customer = () => {
return (
<BananaContext.Consumer>
{(context) => (
<div>
<p>Plantation price: $2</p>
<p>Final price: ${context} ?</p>
</div>
)}
</BananaContext.Consumer>
);
}
useContext()
So how can we use Context API with hooks? With useContext!
import React, { useState, useContext } from 'react';
// First we need to create a Context
const BananaContext = React.createContext();
// Then we need to a Provider Component
const BananaProvider = (props) => {
const [bananaPrice, setBananaPrice] = useState(2); // Original price: $2
return (
<BananaContext.Provider value={bananaPrice}>
{props.children}
</BananaContext.Provider>
);
}
const App = () => {
return ( // Wrap the component in Provider, just like in Redux
<BananaProvider>
<Wholesaler/>
</BananaProvider>
);
}
const Wholesaler = () => {
return <Supermarket/>;
}
const Supermarket = () => {
return <Customer/>;
}
const Customer = () => {
const context = useContext(BananaContext);
return (
<div>
<p>Plantation price: $2</p>
<p>Final price: ${context} 🙂</p>
</div>
);
}
I know you could remove the line where you add the price to the cost of the banana and still get $2 at the end. But that is not the point. The point is that you have to do the props drilling when you don’t use Context. Incrementing the price while passing the the components is sort of the cost to do the props drilling.
Custom Hooks
Why?
Why would we want to create our own hooks? Because of 1 reason – it makes component logic more reusable.
- Custom hook = Reusable logic
How?
How do we create a custom hook? Since hooks are just JavaScript functions, we can create a custom hook by just making a function.
- The only difference is that the function name must start with the word – use. For example, useFunctionName, useHungerStatus etc.
- Custom hooks can call other hooks
Example without custom hook
Say we want to create an application with multiple Stopwatch timers on a single page. How would we do that?
This is what I mean by multiple timers:
Here is the code that implements hooks but does not reuse the logic for the timers
import React, { useEffect, useState } from 'react';
const App = () => {
const [timerOneStatus, setTimerOneStatus] = useState(false);
const [timerOneElapsed, setTimerOneElapsed] = useState(0);
const [timerTwoStatus, setTimerTwoStatus] = useState(false);
const [timerTwoElapsed, setTimerTwoElapsed] = useState(0);
useEffect(() => {
let intervalOne;
if (timerOneStatus) {
intervalOne = setInterval(
() => setTimerOneElapsed(prevTimerOneElapsed => prevTimerOneElapsed + 0.1),
100
);
}
return () => clearInterval(intervalOne);
}, [timerOneStatus]);
useEffect(() => {
let intervalTwo;
if (timerTwoStatus) {
intervalTwo = setInterval(
() => setTimerTwoElapsed(prevtimerTwoElapsed => prevtimerTwoElapsed + 0.1),
100
);
}
return () => clearInterval(intervalTwo);
}, [timerTwoStatus]);
const handleReset1 = () => {
setTimerOneStatus(false);
setTimerOneElapsed(0);
};
const handleReset2 = () => {
setTimerTwoStatus(false);
setTimerTwoElapsed(0);
};
return (
<div>
<div>
<h2>Stopwatch 1</h2>
<h1>{timerOneElapsed.toFixed(1)} s</h1>
<button onClick={() => setTimerOneStatus(!timerOneStatus)}>
{timerOneStatus ? "Stop" : "Start"}</button>
<button onClick={handleReset1}>Reset</button>
</div>
<div>
<h2>Stopwatch 2</h2>
<h1>{timerTwoElapsed.toFixed(1)} s</h1>
<button onClick={() => setTimerTwoStatus(!timerTwoStatus)}>
{timerTwoStatus ? "Stop" : "Start"}</button>
<button onClick={handleReset2}>Reset</button>
</div>
</div>
);
}
As we can see, we are not DRY coding here. Logic for the timers need to repeated every time we want to create a new timer. Imagine if we had 10 timers on one page.
Example with custom hook
Now what we could do is to separate the timer logic as a custom hook and use that one hook for creating any number of timers. Each timer would have its own state and action items. In the main component we use the custom hook just like useState or useEffect and destructure returned parameters from the hook.
import React, { useEffect, useState } from 'react';
const App = () => {
const [timerOneStatus, setTimerOneStatus, timerOneElapsed, resetTimerOne] = useTimer();
const [timerTwoStatus, setTimerTwoStatus, timerTwoElapsed, resetTimerTwo] = useTimer();
return (
<div>
<div>
<h2>Stopwatch 1</h2>
<h1>{timerOneElapsed.toFixed(1)} s</h1>
<button onClick={() => setTimerOneStatus(!timerOneStatus)}>
{timerOneStatus ? "Stop" : "Start"}</button>
<button onClick={() => resetTimerOne()}>Reset</button>
</div>
<div>
<h2>Stopwatch 2</h2>
<h1>{timerTwoElapsed.toFixed(1)} s</h1>
<button onClick={() => setTimerTwoStatus(!timerTwoStatus)}>
{timerTwoStatus ? "Stop" : "Start"}</button>
<button onClick={() => resetTimerTwo()}>Reset</button>
</div>
</div>
);
}
const useTimer = () => {
const [status, setStatus] = useState(false);
const [elapsedTime, setElapsedTime] = useState(0);
useEffect(() => {
let interval;
if (status) {
interval = setInterval(() =>
setElapsedTime(prevElapsedTime => prevElapsedTime + 0.1),
100
);
}
return () => clearInterval(interval);
},[status]);
const handleReset = () => {
setStatus(false);
setElapsedTime(0);
};
return [status, setStatus, elapsedTime, handleReset];
}
In this case, we can place the hook in another file with other custom hooks and call it from anywhere in our project. Much cleaner and more reusable!
Rules with Hooks
Even though hooks are just functions, react team recommends to follow certain rules when using them. If you are lazy or just want to make sure you are following the rules automagically, you can install this linter. However, it is important that we have some common knowledge about the rules.
Rule 1 – Call hooks only at the top level
What does that mean?
It means we should not call hooks inside conditions, loops or nested functions. Rather use them at the top level of React functions.
Rule 2 – Hooks cannot be called from regular JavaScript functions. Call them from React functions
You can call hooks from the following functions:
- Custom hooks
- React function components
Rule 3 – Always start your custom hooks’ name with the word ‘use’
Sample application
Now let us build an application that takes advantage of the most hooks we have covered so far.
We will be building an application called Caturday and it will fetch pictures of random cats on the internet and allow us to vote on the pictures. It will keep count of our likes and dislikes. We will also add a button that can turn our page into dark mode (just change the background color of the page for simplicity).
Here is what the final result will look like: Link to Demo | Github
Step 1
We start building our app by running
$ create-react-app caturday
(If you don’t have create-react-app
module installed, please run npx create-react-app caturday
)
After navigating into the project folder, run
$ npm install semantic-ui-react --save
to install the Semantic UI design tool that will make dealing with CSS much easier.
Step 2
Create 3 files in the /src
folder:
- Caturday.js,
- Caturday.css
- customHooks.js
Step 3
Open the Caturday.css file and copy paste the following:
@media only screen and (max-width: 600px) {
#image-container {
width: 100%;
padding-top: 50px;
}
}
@media only screen and (min-width: 600px) {
#image-container {
width: 50%;
margin: auto;
padding-top: 50px;
}
}
.dark-mode {
background-color: #3b3939;
}
.main-img {
margin: auto;
height: 30em !important;
}
.main-placeholder {
height: 30em !important;
}
Step 5
We create 2 custom hooks to use in our application. Open customHooks.js file and add these hooks
import { useState, useEffect } from "react";
export const useFetchImage = (url, likeCount, mehCount) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(url)
.then(j => j.json())
.then(data => {
console.log(JSON.stringify(data))
setData(data);
setLoading(false);
})
.catch(err => {
console.log(err);
});
}, [likeCount, mehCount]);
return { data, loading };
};
export const useDarkMode = () => {
const [enabled, setEnabled] = useState(false);
useEffect(
() => {
enabled
? document.body.classList.add("dark-mode")
: document.body.classList.remove("dark-mode");
},
[enabled] // Only re-runs when 'enabled' changes
);
return [enabled, setEnabled]; // Return state and setter
};
Step 5
We start constructing the primary file Caturday.js
by importing the css file and creating a container for the app. We will also define the state of component and a customs hooks that update the image url when Like/Dislike buttons are clicked.
import React, { useState, useEffect } from "react";
import { Header, Segment, Image, Placeholder, Button, Container, Label, Icon, Checkbox } from "semantic-ui-react";
import './Caturday.css';
const Caturday = () => {
const [likeCount, setLikeCount] = useState(0);
const [mehCount, setMehCount] = useState(0);
const [darkMode, setDarkMode] = useDarkMode();
const {data, loading} = useFetchImage(
"http://api.thecatapi.com/v1/images/search",
likeCount,
mehCount
);
return (
<Container id="main-container">
<Segment raised>
// Header
// Dark Mode toggle
// Image container
// Like and Dislike buttons
</Segment>
</Container>
);
};
export default Caturday;
Step 6
Now when we define each element of our Caturday
component and put all pieces together, our Caturday.js file should look like this.
import React, { useState, useEffect } from "react";
import { Header, Segment, Image, Placeholder, Button, Container, Label, Icon, Checkbox } from "semantic-ui-react";
import './Caturday.css';
import {useDarkMode, useFetchImage} from './customHooks';
const Caturday = () => {
const [likeCount, setLikeCount] = useState(0);
const [mehCount, setMehCount] = useState(0);
const [darkMode, setDarkMode] = useDarkMode();
const { data, loading } = useFetchImage(
"http://api.thecatapi.com/v1/images/search",
likeCount,
mehCount
);
return (
<Container id="image-container">
<Segment raised>
<Header className="ui basic segment centered">Caturday</Header>
<Segment>
<Checkbox onChange={() => setDarkMode(!darkMode)} toggle floated='right' label="Dark mode"/>
</Segment>
{
loading ?
<Placeholder fluid><Placeholder.Image className="main-placeholder"/></Placeholder> :
<Image src={data[0] ? data[0].url : ''} className="main-img"/>
}
<Segment textAlign="center">
<Button as="div" labelPosition="right">
<Button onClick={() => setLikeCount(likeCount+1)} icon color="green">
<Icon name="heart" />
Like
</Button>
<Label as="a" basic pointing="left">
{likeCount}
</Label>
</Button>
<Button as="div" labelPosition="left">
<Label as="a" basic pointing="right">
{mehCount}
</Label>
<Button onClick={() => setMehCount(mehCount+1)} color="red" icon>
<Icon name="thumbs down" />
Meh
</Button>
</Button>
</Segment>
</Segment>
</Container>
);
};
export default Caturday;
Step 7
Open the App.js
file and replace the return content with Caturday
component
import React from 'react';
import Caturday from './Caturday';
const App = () => {
return (
<Caturday/>
);
}
export default App;
Conclusion
We have covered most of the concepts about hooks and that should be enough for you to get started. If you have a project that you are working on right now that implements states and components in a traditional way, it is absolutely fine. No need to convert everything into hooks. However, when you are about to create new components, just give the hooks a try. They are 100% compatible with regular components with classes. You will be surprised how many lines of code you will be avoiding to accomplish the same functionalities. If you need more information about hooks, please checkout the official documentation by Facebook.