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

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.

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

Working Example