How to write custom React hooks? - Code Exercises
Unlock the full potential of React with custom hooks! In this blog post, we'll go through some exercises that can help you level up your react skills by writing custom hooks in TypeScript. From state management to performance optimization, you'll learn how to create reusable logic for your React components and make your code more efficient and scalable. Every exercise has a brief description of the problem, starting code and relevant links. Try to solve the problems without taking a peek at the solution.
I would suggest using vite-react-app to create a new project for this tutorial. It's a great tool that allows you to create a new React project with a single command.
# Create Vite + React app using npm
npm create vite@latest vite-react-app -- --template react-ts
# Create Vite + React app using yarn
yarn create vite --template react-ts
# Create Vite + React app using pnpm
pnpm create vite --template react-ts
If you need some additional help, you can contact me directly.
Custom React hooks are user-defined functions that allow you to extract and reuse stateful logic and side effects from your React components.
They let you encapsulate and share behavior between multiple components in a clean and reusable way. By using custom hooks, you can reduce the amount of code you write and make it easier to test as well as maintain your React applications.
Custom hooks can also help improve performance by allowing you to share state updates across multiple components and avoid unnecessary re-renders.
Contents
useMousePosition
Write a simple custom hook in TypeScript that keeps track of the mouse position on the screen.
pssst - Use eventListener that listens for 'mousemove' events.
X: 0
Y: 0
Solution(click to show)
import { useState, useEffect } from 'react';
export const useMousePosition = (): [{ x: number, y: number },
React.Dispatch<React.SetStateAction<{ x: number, y: number }>>] => {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
setPosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return [position, setPosition];
};
// Use the hook in a component
import React from 'react';
import { useMousePosition } from './useMousePosition';
const MouseTracker: React.FC = () => {
const [position, setPosition] = useMousePosition();
return (
<div>
<p>X: {position.x}</p>
<p>Y: {position.y}</p>
</div>
);
};
export default MouseTracker;
This code is a simple implementation of a custom hook in React with TypeScript. The custom hook is called useMousePosition
and it returns the current mouse position on the screen, which is represented as an object with x
and y
properties.
The hook uses the useState
and useEffect
hooks from React to manage the state and side effects of the component. The useState
hook is used to create a state variable position
that stores the current mouse position, and a function setPosition
that updates the state.
The useEffect
hook is used to add a mousemove
event listener to the window. The listener updates the state with the current mouse position when the mouse moves. The useEffect
hook also returns a cleanup function that removes the event listener when the component unmounts.
The custom hook is then used in a component called MouseTracker
that displays the current mouse position on the screen. The component uses the useMousePosition
hook to get the current mouse position, and then displays the x
and y
values in a div
element.
Finally, the MouseTracker
component is exported as the default export from the module, making it possible to import and use the component in other parts of the application.
useClickCounter
You need to create a custom hook that tracks the number of clicks on a button in a React component and provides a way for the component to increment the count.
Button has been clicked 0 times
Solution(click to show)
import { useState } from 'react';
export const useClickCounter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(prevCount => prevCount + 1);
return [count, increment] as const;
};
// Use the hook in a component
import React from 'react';
import { useClickCounter } from './useClickCounter';
const ClickCounter: React.FC = () => {
const [count, increment] = useClickCounter();
return (
<div>
<p>Button has been clicked {count} times</p>
<button onClick={increment}>Click me</button>
</div>
);
};
export default ClickCounter;
The useClickCounter
custom hook uses the useState
hook to keep track of the number of clicks on a button in a component. The useState
hook returns an array with two elements: the current state value, and a function to update the state. In this case, the state is the count
of clicks, which is initially set to 0.
The increment
function is created to increment the count when the button is clicked. The function uses the setCount
updater function returned by the useState
hook to update the count by adding 1 to the previous count value.
The hook then returns an array that contains the count
and the increment
function.
The ClickCounter
component imports the useClickCounter
hook and calls it to get the count and increment function. The component then displays the number of clicks and uses the increment function as the onClick
handler for the button.
useClickTracker
Create a custom hook in React that tracks the number of times a button has been clicked and the time of the most recent click. The hook should provide two pieces of state: count
and lastClicked
, and it should also provide a way for the component to increment the count.
Button has been clicked 0 times
Last clicked at: Never
Solution(click to show)
import { useState, useCallback } from 'react';
interface UseClickTrackerState {
count: number;
lastClicked: number | null;
}
export const useClickTracker = (): [UseClickTrackerState, () => void] => {
const [state, setState] = useState<UseClickTrackerState>({
count: 0,
lastClicked: null,
});
const incrementCount = useCallback(() => {
setState(prevState => ({
count: prevState.count + 1,
lastClicked: Date.now(),
}));
}, [setState]);
return [state, incrementCount] as const;
};
The custom hook useClickTracker
provides a way for the component to track the number of times a button has been clicked and the time of the most recent click. The hook exports an array of two elements: state
and incrementCount
. The state
object contains two properties: count
and lastClicked
. The count
property keeps track of the number of times the button has been clicked, and lastClicked
keeps track of the timestamp of the most recent click.
The hook uses the useState
hook to manage the state of the component. The initial state of the component is an object with count
set to 0 and lastClicked
set to null
. The setState
function is used to update the state whenever the button is clicked.
import React from 'react';
import { useClickTracker } from './useClickTracker';
const ButtonClickTracker: React.FC = () => {
const [state, incrementCount] = useClickTracker();
return (
<div>
<p>Button has been clicked {state.count} times</p>
<p>
Last clicked at:{' '}
{state.lastClicked ? new Date(state.lastClicked).toLocaleString() : 'Never'}
</p>
<button onClick={incrementCount}>Click me</button>
</div>
);
};
export default ButtonClickTracker;
The hook also uses the useCallback
hook to ensure that the incrementCount
function is only created once, even if the component is re-rendered multiple times. This can improve performance by avoiding the creation of unnecessary functions.
The component ButtonClickTracker
imports the hook and calls it using the useClickTracker
hook. The hook returns an array with two elements: state
and incrementCount
. The component uses the state
object to display the number of times the button has been clicked and the time of the most recent click. The component also uses the incrementCount
function as the onClick
handler for the button.
useFetchData
Create a custom hook to interact with a REST API and fetch data.
pssst - you can use the JSONPlaceholder API to test your hook.
No data
Solution(click to show)
import { useState, useEffect } from 'react';
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
export const useFetchData = (url: string) => {
const [data, setData] = useState<Post[] | null>(null);
const [error, setError] = useState<{message: string} | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(url);
const data = await response.json();
setData(data);
setLoading(false);
} catch (error) {
setError(error);
setLoading(false);
}
};
fetchData();
}, [url]);
return [data, error, loading] as const;
};
// Use the hook in a component
import React from 'react';
import { useFetchData } from './useFetchData';
const DataFetcher: React.FC = () => {
const [data, error, loading] = useFetchData('https://jsonplaceholder.typicode.com/posts');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!data) return <p>No data</p>;
return (
<ul>
{data.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
};
export default DataFetcher;
The code is a custom React hook named useFetchData
that makes it easier to fetch data from a specified URL and display it in a React component.
The hook uses the useState
hook to manage the state of the data being fetched, an error that may occur, and whether the data is currently being loaded.
The hook uses the useEffect
hook to make an asynchronous call to fetch the data from the URL. If the fetch is successful, it updates the state with the fetched data. If there is an error, it updates the state with the error. It also updates the loading state while the data is being fetched.
In a component, the hook is used by calling useFetchData
and passing in the URL to fetch data from. The hook returns an array with the data, error, and loading state, which can be destructured into separate variables.
The component then uses conditional rendering to display either a "Loading..." message, an error message, or the fetched data. The fetched data is displayed as a list of items, each item being a title from the data.
useDebouncedSearch
Create a custom hook that implements a debounced search feature. The hook should receive an input value and a function that makes a search. The hook should debounce the search function calls, waiting for the user to stop typing for a certain amount of time before actually executing the search.
Debounced search term:
Solution(click to show)
import { useState, useEffect } from 'react';
export const useDebouncedSearch = (
value: string,
search: (value: string) => void,
wait: number
) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, wait);
return () => {
clearTimeout(handler);
};
}, [value, wait]);
useEffect(() => {
search(debouncedValue);
}, [debouncedValue, search]);
return [debouncedValue];
};
// Use the hook in a component
import React, { useState } from 'react';
import { useDebouncedSearch } from './useDebouncedSearch';
const SearchComponent: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebouncedSearch(searchTerm, handleSearch, 500);
const handleSearch = (value: string) => {
console.log(`Searching for: ${value}`);
};
return (
<div>
<input
type="text"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
<p>Debounced search term: {debouncedSearchTerm}</p>
</div>
);
};
export default SearchComponent;
The custom hook useDebouncedSearch
allows you to implement a debounced search feature in your component. It takes three parameters: value
, search
, and wait
.
The hook uses two useEffect
hooks to create the debouncing behavior. The first useEffect sets a timeout whenever the value or wait parameters change. The timeout updates the state debouncedValue
with the latest value after waiting for wait milliseconds.
The second useEffect hook then executes the search function with the latest debouncedValue
whenever it changes.
In the SearchComponent
component, we use the useDebouncedSearch
hook and pass in the input value, a handleSearch
function, and a wait time of 500 milliseconds. The component also has a state searchTerm
that updates with the input value.
The useDebouncedSearch
hook returns the debouncedSearchTerm
state, which we use to display in the component. The component also calls the handleSearch
function with the debounced search term whenever it changes.
With this custom hook, the search function will only be executed after the user stops typing for 500 milliseconds, rather than being called with every keystroke.