React hooks have been a game changer for developers since they were introduced in React 16.8. Hooks give developers access to state and other React features without using class components. This tutorial will discuss how to create custom React hooks.
Prerequisites
To understand this tutorial, you should have a basic understanding of React, JavaScript, and ES6 syntax. It's also recommended that you have some experience using hooks.
A Quick Overview of React Hooks
Before hooks came along, state and other React features could only be used in class components. With hooks, you can create reusable stateful logic that can be shared across multiple components. Some of the commonly used React hooks are useState, useEffect, useContext, and useRef.
Rules Guiding Usage of React Hooks
There are a couple of rules that should be followed when creating and using hooks:
- Names of hooks must start with "use" to indicate that it is a hook.
const useMyHook = () => {/*....*/}
const useAnotherHook = () => {/*....*/}
- Hooks should only be used at the top level of a functional component or another hook. They should not be used inside loops, conditions, or nested functions as they can lead to unexpected behavior.
const MyComponent = () => {
const items = [];
if(items.length > 2) {
useMyHook(); // do not do this
}
items.forEach(item => useMyHook()); // do not do this
return <p>My component</p>
}
- Hooks must only be called from within a functional component or another hook. They cannot be called from regular JavaScript functions.
const myFunction = () => {
const hookResult = useMyHook(); // wrong!
}
const useMyHook = () => {
const other = useMyOtherHook(); // correct!
// .............
// .............
}
const MyComponent = () => {
const myHook = useMyHook();
return <p>My Component</p>
}
Why Create Custom Hooks
Creating custom hooks is useful for quite a number of reasons:
- Custom hooks allow us to extract and reuse stateful logic in our React components. By encapsulating stateful logic in a custom hook, we can make our code more modular and easier to maintain.
- Custom hooks allow us to separate concerns in our code, making it easier to read and test.
Creating a Custom Hook
Now that we have a reason to use custom hooks, let’s create three hooks and see them in action:
useWindowWidth
useLocalStorage
useFetch
💡 Note: You can find the code for each hook in the sandbox.
useWindowWidth
useWindowWidth
allows us to get the current width of the window. It uses the resize
event to update the window's width whenever the window is resized:
// /src/hooks/useWindowWidth.js
import { useState, useEffect } from "react";
// notice that the hook name starts with `use`
export const useWindowWidth = () => {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return { windowWidth };
};
With this hook present, we don’t have to write the logic for resizing the window every time we want to get the window’s width in a component.
Also notice that we are returning a value. It’s common but not compulsory to return any JavaScript data type from a hook. We are returning the windowWidth
in an object so that it will be easily destructured.
We will call our hook at the top level of any component that wants to use it:
// /src/components/Window.jsx
import { useWindowWidth } from "./../hooks/useWindowWidth";
const Window = () => {
const { windowWidth } = useWindowWidth();
return (
<section>
<h1>Window width:</h1>
<p>{windowWidth}</p>
</section>
);
};
export default Window;
useLocalStorage
useLocalStorage
allows us to store data in the browser’s local storage and retrieve it in a different component:
// /src/hooks/useWindowWidth.js
import { useState } from "react";
export const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(() => {
const localVal = localStorage.getItem(key);
return localVal === null ? initialValue : JSON.parse(localVal);
});
const updateValue = (newValue) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
};
return { value, updateValue };
};
Just like normal functions, custom hooks can also accept any numbers of arguments. We passed in the key
and an initial value
in this case:
-
key
represents the local storage key. -
initialValue
represents a default value to use if there is no value for thekey
in the local storage.
Notice we are returning a value
and function
this time around. The updateValue
can be called just like every other JavaScript function.
We can then use our hook like this:
// /src/components/Stack.jsx
import { useLocalStorage } from "./../hooks/useLocalStorage";
const Stack = () => {
const { value: stack, updateValue: updateStack } = useLocalStorage(
"stack",
"JS"
);
return (
<section>
<h1> Stack: </h1>
<p>{stack}</p>
<button onClick={() => updateStack(stack === "JS" ? "TS" : "JS")}>
Update stack
</button>
</section>
);
};
useFetch
useFetch
fetches data asynchronously. It returns these states:
-
loading
determines if the request is still processing. -
data
represents the resource we requested. -
error
represents an error message.
// /src/hooks/useFetch.js
import { useEffect, useState } from "react";
export const useFetch = (url) => {
const [resource, setResource] = useState({
loading: true,
error: null,
data: null
});
useEffect(() => {
const fetchData = async () => {
try {
const req = await fetch(url);
const data = await req.json();
setResource((prev) => ({ ...prev, data }));
} catch (error) {
setResource((prev) => ({ ...prev, error: error.message }));
} finally {
setResource((prev) => ({ ...prev, loading: false }));
}
};
fetchData();
}, []);
return resource;
};
💡 Note: This useFetch isn't production ready.
We can then call our hook in any component like this:
// /src/components/Posts.jsx
import { useFetch } from "./../hooks/useFetch";
const postsUrl = "<https://jsonplaceholder.typicode.com/posts>";
const Posts = () => {
const { data, loading, error } = useFetch(postsUrl);
if (loading) return <div>Loading</div>;
if (error) return <div>Error</div>;
const posts = [...data].slice(1, 10);
return (
<section>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.body}</li>
))}
</ul>
</section>
);
};
export default Posts;
Custom Hook Share Stateful Logic, not State itself
Custom hooks are meant to share stateful logic, and not state itself. Two different components using the same custom hook are not sharing the same state that the custom hook provides. This means that you should not use a custom hook to share the actual state between components. When you need to share the state itself between multiple components, lift it up to a parent and pass it down instead.
Let's look at our useLocalStorage
for an example. Assume that we have another component, Profile
, that calls the hook like this:
// /src/components/Profile.jsx
import { useLocalStorage } from "../hooks/useLocalStorage";
const Profile = () => {
const { value: gender, updateValue: updateGender } = useLocalStorage(
"gender",
"Male"
);
return (
<section>
<h1> Gender: </h1>
<p>{gender}</p>
<button
onClick={() => updateGender(gender === "Male" ? "Female" : "Male")}
>
Update gender
</button>
</section>
);
};
export default Profile;
The Stack
and Profile
components are not sharing the same state even though they are calling the useLocalStorage
hook. Updating our stack with the function returned by useLocalStorage
won't update the gender, likewise updating the gender
won't update the stack
.
Conclusion
Custom React hooks are a powerful tool for creating reusable stateful logic in your React applications. They allow you to separate concerns and make your code more modular and easier to maintain. By following the rules for using hooks and starting with simple examples, you can create custom hooks that are easy to use.
You can read more about custom hooks from the react documentation.
Happy coding.
Top comments (0)