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.

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

Comments