Skip to content

Custom Hooks

Video Lecture

Section Video Links
Custom Hooks : Part 1 Custom Hooks : Part 1 Custom Hooks : Part 1 
Custom Hooks : Part 2 Custom Hooks : Part 2 Custom Hooks : Part 2 

 (Pay Per View)

You can use PayPal to purchase a one time viewing of this video for $1.49 USD.

Pay Per View Terms

  • One viewing session of this video will cost the equivalent of $1.49 USD in your currency.
  • After successful purchase, the video will automatically start playing.
  • You can pause, replay and go fullscreen as many times as needed in one single session for up to an hour.
  • Do not refresh the browser since it will invalidate the session.
  • If you want longer-term access to all videos, consider purchasing full access through Udemy or YouTube Memberships instead.
  • This Pay Per View option does not permit downloading this video for later viewing or sharing.
  • All videos are Copyright © 2019-2025 Sean Bradley, all rights reserved.

Description

In this example, we will create a minimal hook that listens for keyboard presses.

Part 1

In the first part of this lesson, we will use the key presses to move the box. The Box will move at 1 unit per second.

./src/useKeyboard.jsx

import { useEffect, useRef } from 'react'

export default function useKeyboard() {
  const keyMap = useRef({})

  useEffect(() => {
    const onDocumentKey = (e) => {
      keyMap.current[e.code] = e.type === 'keydown'
    }
    document.addEventListener('keydown', onDocumentKey)
    document.addEventListener('keyup', onDocumentKey)
    return () => {
      document.removeEventListener('keydown', onDocumentKey)
      document.removeEventListener('keyup', onDocumentKey)
    }
  }, [])

  return keyMap.current
}

./src/Box.jsx

import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
import useKeyboard from './useKeyboard'

export default function Box(props) {
  const ref = useRef()
  const keyMap = useKeyboard()

  useFrame((_, delta) => {
    keyMap['KeyA'] && (ref.current.position.x -= 1 * delta)
    keyMap['KeyD'] && (ref.current.position.x += 1 * delta)
    keyMap['KeyW'] && (ref.current.position.z -= 1 * delta)
    keyMap['KeyS'] && (ref.current.position.z += 1 * delta)
  })

  return (
    <mesh ref={ref} {...props}>
      <boxGeometry />
      <meshBasicMaterial color={0x00ff00} wireframe />
    </mesh>
  )
}

./src/App.jsx

import { Canvas } from '@react-three/fiber'
import Box from './Box'
import { Stats, OrbitControls } from '@react-three/drei'

export default function App() {
  return (
    <Canvas camera={{ position: [1, 2, 3] }}>
      <Box position={[0, 0.5, 0]} />
      <OrbitControls />
      <axesHelper args={[5]} />
      <gridHelper />
      <Stats />
    </Canvas>
  )
}

Part 2

In the second part of this lesson, we add more boxes to the scene, and be able to move 1 or more boxes at the same time.

./src/Box.jsx

import { useRef, useState } from 'react'
import { useFrame } from '@react-three/fiber'

export default function Box(props) {
  const ref = useRef()
  const keyMap = props.keyMap
  const [selected, setSelected] = useState(props.selected)

  useFrame((_, delta) => {
    keyMap['KeyA'] && selected && (ref.current.position.x -= 1 * delta)
    keyMap['KeyD'] && selected && (ref.current.position.x += 1 * delta)
    keyMap['KeyW'] && selected && (ref.current.position.z -= 1 * delta)
    keyMap['KeyS'] && selected && (ref.current.position.z += 1 * delta)
  })

  return (
    <mesh ref={ref} {...props} onPointerDown={() => setSelected(!selected)}>
      <boxGeometry />
      <meshBasicMaterial color={0x00ff00} wireframe={!selected} />
    </mesh>
  )
}

./src/App.jsx

import { Canvas } from '@react-three/fiber'
import Box from './Box'
import { Stats, OrbitControls } from '@react-three/drei'
import useKeyboard from './useKeyboard'

export default function App() {
  const keyMap = useKeyboard()

  return (
    <Canvas camera={{ position: [1, 2, 3] }}>
      <Box position={[-1.5, 0.5, 0]} keyMap={keyMap} />
      <Box position={[0, 0.5, 0]} keyMap={keyMap} selected />
      <Box position={[1.5, 0.5, 0]} keyMap={keyMap} />
      <OrbitControls />
      <axesHelper args={[5]} />
      <gridHelper />
      <Stats />
    </Canvas>
  )
}

Rules of Hooks

When creating hooks versus components, remember to follow these naming conventions.

  • Hook names must start with use followed by a capital letter, like useState (built-in) or useKeyboard. The use prefix enables the linter plugin to automatically check for violations of rules of Hooks.

  • React component names, versus hooks, must start with a capital letter, such as in App, Box and Polygon.

Clean Up

If using useEffect within your hook, you can optionally specify how to "clean up" after them by returning a callback. For example, in this useKeyboard example, when it is unloaded, the useEffect completes, and removes the keyDown and keyUp events from the HTML DOM.

If I didn't provide the option to remove the keyDown and keyUp event listeners, then every time this component was refreshed, due to a state or props change, then the DOM would keep accumulating new event listeners on each re-render of the component that uses this hook.

Execute getEventListeners(document) in the browser console, to see how many event listeners are currently attached to your HTML DOM.

Working Example

<>
Custom Hooks reactjs.org
Rules of Hooks reactjs.org
HTML DOM w3schools.com

GitHub Branch

git clone https://github.com/Sean-Bradley/React-Three-Fiber-Boilerplate.git
cd React-Three-Fiber-Boilerplate
git checkout customHook
npm install
npm start