React hooks - Best practices and advanced application
What are React hooks?link
React Hooks are a new feature in React 16.8 that lets you use state and other React features without building a class-based component. They are used in functional components and make it easier to reuse stateful logic. This in turn helps to make your code more readable and maintainable.
With that said, let's dive into the best practices when building hooks in React!
Best practices for building React hookslink
If you're looking to build your own hooks in React, there are a few things you need to keep in mind. The list below aims to set out som basic guidelines to follow.
-
Prefix your custom hooks with the word "use". This will help other developers identify them as hooks.
-
Follow the Rules of Hooks - only call hooks at the top level of your component and don't call them inside loops, conditions, or nested functions. Also, make sure that you only call your hooks from React functions!
-
Break down your logic into smaller chunks. For example, if you're building a login form, you might have one chunk of logic for handling the user input and another for submitting the form. By breaking down your logic into smaller pieces, it will be easier to understand, manage and test.
-
Use in-built hooks correctly. For example, the
useEffect
hook is great for side effects like making network requests or subscribing to a data source. However, it's important to note thatuseEffect
runs on every render by default. If you don't want your code to run on every render, you can pass an array of dependencies as the second argument. This will tell React to only run the effect if one of the dependencies has changed. -
If you need to share state between multiple components, it's sometimes better to use the
useContext
hook instead of creating a custom hook. -
Don't be afraid to refactor your code as you go along. As your application grows, so will the complexity of your hooks. Nothing is perfect the first time, and refactoring is a part of continuous improvement.
Advanced applications of React hookslink
A good example to use to produce our custom hook is fetching data from an API. There are a lot of libraries out there that will take care of this functionality for us, however, this exercise will cover some core concepts.
So let's look at this basic component. It requests data from an API to fetch the latest HackerNews articles on the first render, and then displays them to the end-user.
import { useEffect, useState } from 'react'
export default function PostsComponent() {
const [posts, setPosts] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
const fetchData = async () => {
setLoading(true)
try {
const data = await fetch('https://hn.algolia.com/api/v1/search')
const parsed = await data.json()
setPosts(parsed)
} catch (err) {
setError(err.responseText)
}
setLoading(false)
}
fetchData()
}, [])
if (loading) return <div>Loading</div>
if (error) return <div>Error!! ${error}</div>
return (
<ul>
{posts.map(({ title, url }) => (
<li key={title}>
<a href={url} title={title}>
{title}
</a>
</li>
))}
</ul>
)
}
This component works fine in isolation. However, we're making network requests throughout our project, and it would be handy to simplify this logic in some way so that we don't have to keep repeating it. So let's begin by creating a simple useFetch
hook:
// hooks/use-fetch.js
import { useEffect, useState } from 'react'
export default function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
const fetchData = async () => {
setLoading(true)
try {
const response = await fetch(url)
const parsed = await response.json()
setData(parsed)
} catch (err) {
setError(err.responseText)
}
setLoading(false)
}
fetchData()
}, [url])
return {
loading,
error,
data,
}
}
// components/post-component.jsx
import useFetch from '../hooks/use-fetch'
export default function PostsComponent() {
const { loading, data, error } = useFetch('https://hn.algolia.com/api/v1/search')
const posts = data?.hits || []
if (loading) return <div>Loading</div>
if (error) return <div>Error!! ${error}</div>
return (
<ul>
{posts.map(({ title, url }) => (
<li key={title}>
<a href={url} title={title}>
{title}
</a>
</li>
))}
</ul>
)
}
Now we have a hook that we can recycle and use for other requests! if the URL passed to the hook changes, it will automatically make a new network request.
But wait, we can simplify this hook further, and even make it more performant. By using useReducer
, we can consolidate the state changes that are made into a single action. Let's create a reducer function that will consume generic response data, and manages the main request states that we care about:
// hooks/use-fetch.js
import { useEffect, useReducer } from 'react'
function fetchReducer(state, action) {
switch (action.type) {
case 'LOADING': {
return { loading: true, data: null, error: null }
}
case 'LOADED': {
return { loading: false, data: action.data, error: null }
}
case 'ERROR': {
return { loading: false, data: null, error: action.error }
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
export default function useFetch(url) {
const [state, dispatch] = useReducer(fetchReducer, {
data: null,
loading: false,
error: null,
})
useEffect(() => {
const fetchData = async () => {
dispatch({ type: 'LOADING' })
try {
const response = await fetch(url)
const parsed = await response.json()
dispatch({ type: 'LOADED', data: parsed })
} catch (err) {
dispatch({ type: 'ERROR', error: err.responseText })
}
}
fetchData()
}, [url])
return state
}
We've now created a simple reducer that handles each of our use-cases, and only a single update needs to be made each time. Much better!
Conclusionlink
So there you have it! These are just a few tips to keep in mind when building hooks in React. By following these tips, you can make sure your hooks are well organized, easy to understand, and performant.