custom hooks

useKeyPress( )

The code provided in this article is simplified as much as possible to avoid overwhelming the reader with code and to convey the essence of the problem and its solution. In interactive examples, heading tags (`h1...h6`) are replaced with span tags to prevent potential conflicts with the main page structure.

Explanation

The useKeyPress custom React hook allows you to monitor the state of a specific keyboard key, indicating whether it's currently pressed. Additionally, it provides the flexibility to specify a callback function that gets triggered whenever the designated key is pressed. It offers versatility, supporting two distinct usage patterns:

  1. Without passing a callback function, allowing for direct access to the key press state in the component.
  2. By passing a callback function, facilitating event-driven actions triggered by key presses. In this case, there's no necessity to return a value if it's not needed.

This hook enhances user interaction by enabling developers to create responsive and dynamic UI components that respond to keyboard input. By encapsulating keyboard event handling logic, useKeyPress promotes code reusability and improves the maintainability of React applications.

It efficiently manages event listeners, ensuring proper cleanup when the component unmounts or when the target key changes. Its simplicity lies in its ability to abstract away the complexities of event handling, allowing developers to focus on implementing desired behavior for specific keys without worrying about low-level event management. With useKeyPress, React developers can seamlessly integrate keyboard interaction into their applications, enhancing user experience and interactivity with minimal effort.

Usage Cases

Without Passing a Callback Function

  • Conditional Rendering: Enable or disable rendering of specific elements based on key presses.
  • Focus Management: Shift focus between elements or components upon pressing a designated key.
  • Navigation Control: Navigate between different sections or pages within the application based on key input.

By Passing a Callback Function

  • Keyboard Event Handling: Execute specific actions or handle events upon pressing a designated key, such as submitting user input when it isn't wrapped in a form tag.
  • Enhanced Interaction: Implement interactive features that respond to specific key presses, such as triggering animations or toggling display modes.
  • Accessibility Enhancements: Implement keyboard shortcuts to improve accessibility for users with disabilities, facilitating easier navigation and interaction with the application.
  • Hotkeys for Function Access: Use hotkeys for quick access to essential application functions, for example, pressing F to open the search bar.
  • Video Players: Control video playback using keys such as Space for pause/play and arrows for seeking.
  • Modal Dialogs: Close modal dialogs or pop-ups by pressing the Escape key.

Creation

Code implementation
1const useKeyPress = (targetKey: string, handler?: () => void): boolean => {
2 // Initialize the state for keeping track of whether key is pressed
3 const [isKeyPressed, setIsKeyPressed] = useState(false);
4
5 // Create a `ref` that stores the latest `handler` function if provided
6 const latestHandler = useRef(handler ?? null);
7
8 // Update `latestHandler.current` when the `handler` changes.
9 // This ensures our `useEffect` below always gets the latest `handler` without
10 // needing to pass it in its dependencies array,
11 // which would cause the `useEffect` to re-run after every component render
12 // in case the `handler` is created from scratch inside it
13
14 // `useLayoutEffect` is used here to guarantee that the event handler
15 // reference is updated before any subsequent renders or event handling take place.
16 // Since there's a possibility that when using `useEffect`, at a certain moment,
17 // the handler might become outdated. This is because `useEffect` is asynchronous:
18 // its code executes after the browser finishes rendering. Consequently,
19 // the handler will be updated after the browser's initial rendering.
20 // In contrast, `useLayoutEffect` is synchronous, ensuring that its code executes
21 // after all DOM mutations but before the browser updates the interface
22 useLayoutEffect(() => {
23 // Check if `handler` provided
24 if (handler) {
25 // Update `latestHandler` when the `handler` (its reference) changes
26 latestHandler.current = handler;
27 }
28 }, [handler]);
29 // By using `useRef` and `useLayoutEffect`, this technique eliminates the need
30 // to memoize the handler outside the hook (in the component) via `useCallback`.
31 // It abstracts the optimization logic inside the hook, making it more convenient
32
33 // Set up an effect to attach event listeners for `keydown` and `keyup` events
34 // when the component mounts or when the `targetKey` changes
35 useEffect(() => {
36 // Define event handler functions to set the `isKeyPressed` state accordingly.
37 // By defining them within the effect, we avoid the need
38 // to include them in the dependency array
39
40 const downHandler = ({ key }: KeyboardEvent) => {
41 // If pressed key is a `targetKey`, then set to `true`
42 if (key === targetKey) {
43 setIsKeyPressed(true);
44
45 // Call handler function if provided
46 if (latestHandler.current) {
47 latestHandler.current();
48 }
49 }
50 };
51
52 const upHandler = ({ key }: KeyboardEvent) => {
53 // If released key is a `targetKey`, then set to `false`
54 if (key === targetKey) {
55 setIsKeyPressed(false);
56 }
57 };
58
59 // Attach event listeners for `keydown` and `keyup` events to the window
60 window.addEventListener('keydown', downHandler);
61 window.addEventListener('keyup', upHandler);
62
63 // Return a cleanup function to remove event listeners
64 // when the component unmounts or when the `targetKey` changes
65 return () => {
66 window.removeEventListener('keydown', downHandler);
67 window.removeEventListener('keyup', upHandler);
68 };
69
70 // Include only the `targetKey` in the dependency array to ensure the `useEffect` runs when it changes.
71 // Although we use `latestHandler` inside `useEffect`, it's not necessary
72 // to include it in the dependencies array. `useEffect` knows that the reference to `latestHandler`
73 // doesn't change, so it won't re-run unless `targetKey` changes or the component unmounts
74 }, [targetKey]);
75
76 return isKeyPressed;
77};
78
79export default useKeyPress;
80

Reference

Code implementation
1const isKeyPressed = useKeyPress(targetKey, handler?)
2

Parameters

NameTypeDescription
targetKeystringThe proper key name to listen for keyboard presses.
handler (optional)() => voidA callback function to execute when the specified key is pressed.

Return Values

NameTypeDescription
isKeyPressedbooleanA boolean value indicating whether the specified key is currently pressed.

Example Usages

Keyboard-Controlled Image Gallery

This example demonstrates how to use keyboard navigation to control an interactive gallery. Instead of actual images, the Square component is used to simulate items in the gallery. Users can navigate through the items using the ArrowLeft and ArrowRight keys. The useKeyPress hook listens for these key presses and triggers the respective functions to move to the previous or next item. The currently selected item is visually highlighted, and the corresponding item's ID is displayed prominently.

This approach enhances user experience by providing an intuitive way to navigate the gallery. Designed with accessibility in mind, the component ensures users can easily interact with the gallery using keyboard inputs.

Code implementation
1const KEY_PREV = 'ArrowLeft';
2const KEY_NEXT = 'ArrowRight';
3
4const initialItemId = 1;
5
6const items = loadData(4);
7
8const Gallery: FC = () => {
9 // State to track the current index of the image
10 const [currentItem, setCurrentItem] = useState(initialItemId);
11
12 // Function to handle moving to the previous image in the gallery
13 const handlePrev = () => {
14 if (currentItem === 1) return;
15
16 setCurrentItem((prev) => prev - 1);
17 };
18
19 // Function to handle moving to the next image in the gallery
20 const handleNext = () => {
21 if (currentItem === items.length) return;
22
23 setCurrentItem((prev) => prev + 1);
24 };
25
26 // Call the `useKeyPress` hook to handle the arrow keys presses
27
28 // Trigger `handlePrev` function when the `ArrowLeft` key is pressed
29 useKeyPress(KEY_PREV, handlePrev);
30
31 // Trigger `handleNext` function when the `ArrowRight` key is pressed
32 useKeyPress(KEY_NEXT, handleNext);
33
34 return (
35 <Layer title="Gallery">
36 <div className="flex flex-col gap-2">
37 <Square className="font-monoBrand pointer-events-none text-6xl font-bold">
38 <span>{currentItem}</span>
39 </Square>
40 <div className="flex w-full space-x-2">
41 {items.map((item) => (
42 <Square
43 className="pointer-events-none"
44 key={item.id}
45 colored={currentItem === item.id}
46 >
47 <span className="font-monoBrand text-3xl font-bold">
48 {item.id}
49 </span>
50 </Square>
51 ))}
52 </div>
53 </div>
54 <Message>
55 <span>Press one of the following keys on your keyboard:</span>
56 <div className="flex space-x-4 py-2">
57 <Kbd id={KEY_PREV} />
58 <Kbd id={KEY_NEXT} />
59 </div>
60 </Message>
61 </Layer>
62 );
63};
64
An interactive output of the code above

Modal with Escape Key Handling

This example demonstrates the creation of a modal component that incorporates keyboard accessibility and user experience enhancements. The ModalWrapper component listens for the Escape key press using the useKeyPress hook to close the modal, providing a convenient way for users to dismiss it without relying solely on mouse interactions. The Container component manages the modal's state, using the isOpen flag to conditionally render the modal. The modal itself features two close options: pressing the Escape key or clicking the "Close Modal" button. This design improves usability and accessibility, catering to a wide range of user preferences and needs.

Code implementation
1interface ModalProps {
2 onClose: () => void;
3}
4
5const KEY_CLOSE = 'Escape';
6
7const ModalWrapper: FC<ModalProps> = ({ onClose: handleClose }) => {
8 // Call the `useKeyPress` hook to handle the `Escape` key press and close the modal
9 useKeyPress(KEY_CLOSE, handleClose);
10
11 // Call the hook to prevent page scrolling when the modal component is mounted
12 useLockBodyScroll();
13
14 return (
15 <Modal>
16 <Message>
17 <span>
18 Press the following key on your keyboard to close the modal
19 window:
20 </span>
21 <Kbd id={KEY_CLOSE} />
22 <span>Or click the following button:</span>
23 </Message>
24 <Button onClick={handleClose}>Close Modal</Button>
25 </Modal>
26 );
27};
28
29const Container: FC = () => {
30 // State to track whether the modal is open or not
31 const [isOpen, setIsOpen] = useState(false);
32
33 // Function to handle closing the modal
34 const handleClose = () => {
35 setIsOpen(false);
36 };
37
38 // Function to handle opening the modal
39 const handleOpen = () => {
40 setIsOpen(true);
41 };
42
43 return (
44 <Layer title="Container">
45 <div className="flex w-full justify-center">
46 <Button onClick={handleOpen}>Open Modal</Button>
47 </div>
48 {/* Conditional rendering of modal based on `isOpen` state */}
49 {isOpen && <ModalWrapper onClose={handleClose} />}
50 </Layer>
51 );
52};
53
An interactive output of the code above

Quiz Answer Reveal with Keyboard Shortcut

This example showcases an interactive quiz component that allows users to reveal answers dynamically by pressing a designated key (Shift). The useKeyPress hook is used to detect whether the Shift key is pressed. The Container component maintains the state for the current question index and provides navigation through a "Next Question" button.

This implementation demonstrates how to create an engaging and intuitive user experience by leveraging keyboard interaction to enhance accessibility and interactivity. The design ensures ease of use while keeping the interface clean and focused.

Code implementation
1const KEY_SHOW_ANSWER = 'Shift';
2
3// Quiz data containing questions and answers
4const questions = loadData();
5
6const Container: FC = () => {
7 // State to track the current index of the question
8 const [currentIndex, setCurrentIndex] = useState(0);
9
10 // Call the `useKeyPress` hook without passing a callback function
11 // to detect whether the `Shift` key is pressed by returning the
12 // boolean value, which is then used for conditional rendering
13 const isShiftPressed = useKeyPress(KEY_SHOW_ANSWER);
14
15 // Function to handle moving to the next question in the quiz
16 const handleNext = () => {
17 if (currentIndex === questions.length - 1) {
18 setCurrentIndex(0);
19
20 return;
21 }
22
23 setCurrentIndex((prev) => prev + 1);
24 };
25
26 const currentQuestion = questions[currentIndex];
27
28 return (
29 <Layer title="Container">
30 <Text className="md:h-24">{currentQuestion.question}</Text>
31 <Text>
32 Answer: {isShiftPressed ? currentQuestion.answer : 'β‰οΈπŸ˜Έβ‰οΈ'}
33 </Text>
34 <div className="flex w-full justify-center">
35 <Button onClick={handleNext}>Next Question</Button>
36 </div>
37 <Message>
38 <span>
39 Press the following key on your keyboard to reveal the
40 correct answer:
41 </span>
42 <Kbd id={KEY_SHOW_ANSWER} />
43 </Message>
44 </Layer>
45 );
46};
47
An interactive output of the code above
Last updated on by @skrykateSuggest an improvement on Github repository.