Events
Compare manual raycasting and pointer bookkeeping with R3F's built-in pointer event system.
React Three Fiber Three.js
Pinned example stack
react@19.1.1react-dom@19.1.1@react-three/fiber@9.3.0three@0.180.0
Exact Conversion
Switch between the React Three Fiber and Three.js examples, then test the R3F result in the live preview below.
import { useState } from 'react';import { Canvas } from '@react-three/fiber';
function ClickableBox({ color, position }) { const [active, setActive] = useState(false);
return ( <mesh position={position} scale={active ? 1.35 : 1} onClick={() => setActive((value) => !value)}> <boxGeometry /> <meshStandardMaterial color={color} /> </mesh> );}
export default function App() { return ( <div style={{ height: '100vh', background: '#111827' }}> <Canvas camera={{ fov: 55, position: [0, 0.5, 5] }} dpr={[1, 2]}> <color attach="background" args={['#111827']} /> <directionalLight position={[2, 3, 4]} intensity={1.6} /> <ClickableBox color="#f97316" position={[-1.1, 0, 0]} /> <ClickableBox color="#38bdf8" position={[1.1, 0, 0]} /> </Canvas> </div> );}import * as THREE from 'three';
const scene = new THREE.Scene();scene.background = new THREE.Color('#111827');
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 100);camera.position.set(0, 0.5, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));renderer.setSize(window.innerWidth, window.innerHeight);document.body.style.margin = '0';document.body.appendChild(renderer.domElement);
const raycaster = new THREE.Raycaster();const pointer = new THREE.Vector2();
const materialA = new THREE.MeshStandardMaterial({ color: '#f97316' });const materialB = new THREE.MeshStandardMaterial({ color: '#38bdf8' });
const leftBox = new THREE.Mesh(new THREE.BoxGeometry(), materialA);leftBox.position.x = -1.1;
const rightBox = new THREE.Mesh(new THREE.BoxGeometry(), materialB);rightBox.position.x = 1.1;
const light = new THREE.DirectionalLight('#ffffff', 1.6);light.position.set(2, 3, 4);
scene.add(light, leftBox, rightBox);
window.addEventListener('pointerdown', (event) => { pointer.x = (event.clientX / window.innerWidth) * 2 - 1; pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera); const [hit] = raycaster.intersectObjects([leftBox, rightBox]);
if (hit?.object) { const mesh = hit.object; const isExpanded = mesh.scale.x > 1; mesh.scale.setScalar(isExpanded ? 1 : 1.35); }
renderer.render(scene, camera);});
function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.render(scene, camera);}
window.addEventListener('resize', onResize);renderer.render(scene, camera);Live preview
What Changed
- In raw Three.js, you often manage a
Raycaster, normalized pointer coordinates, and manual intersections yourself. - In R3F, a mesh can often handle
onClick,onPointerOver, and related events directly. - The clicked object is still the same underlying Three.js mesh.
- React state is fine here because the interaction is discrete, not per-frame.
When To Drop Back Down
Section titled “When To Drop Back Down”Built-in R3F events cover a lot of interaction work. If you need fully custom hit testing or more complex control over intersections, you can still use the raw Three.js tools underneath.