When building interactive 3D applications with React and Three.js, React Hooks act as the bridge between UI logic and the rendering engine. This article explains hooks in three layers:
- The general React Hooks concept
- Real usage inside a Three.js project, step by step
- Which hooks matter most in production apps
🧠 1. React Hooks — Quick Overview
React Hooks help functional components manage:
- 🟢 State — data that changes the UI
- 🔵 Side effects — external operations
- 🟣 Memoization — performance optimization
🟢 useState → UI state. Used for values that change and affect rendering: the selected object, the selected color, movement controls.
🔵 useEffect → side effects. Used for operations outside React rendering: initializing the Three.js scene, attaching events, syncing React → Three.js.
🟣 useRef → persistent storage. Used to store DOM elements, the Three.js scene, camera and renderer, and mesh references — without causing a re-render.
🟡 useMemo → performance optimization. Avoids expensive recalculations and caches geometry or derived values.
🔴 useCallback → stable functions. Prevents function recreation; useful for event listeners such as resize and click.
🎮 2. React Hooks in a Real Three.js Project
Now let's map everything onto a real 3D system, step by step.
Step 1: useRef → the Three.js engine layer
We store core Three.js objects outside the React render cycle:
const sceneRef = useRef<THREE.Scene | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
const cubesRef = useRef<THREE.Mesh[]>([]);Why? These objects are not UI state, they should not trigger a re-render, and they must persist across renders.
Step 2: useState → the UI layer
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [selectedColor, setSelectedColor] = useState("#ff9900");
const [moveX, setMoveX] = useState(0);These drive UI updates, control interaction and trigger effects.
Step 3: useMemo → geometry optimization
Static (created once) — geometry is expensive, so we create it only once:
const baseGeometry = useMemo(() => {
return new THREE.BoxGeometry(1, 1, 1);
}, []);Dynamic (depends on state) — a value derived from state:
const dynamicOffset = useMemo(() => {
return moveX * 0.5;
}, [moveX]);Step 4: useCallback → stable functions
Resize handler (no dependencies):
const handleResize = useCallback(() => {
const camera = cameraRef.current;
const renderer = rendererRef.current;
if (!camera || !renderer || !mountRef.current) return;
camera.aspect = mountRef.current.clientWidth / mountRef.current.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(mountRef.current.clientWidth, mountRef.current.clientHeight);
}, []);Click handler (depends on state) — the click behavior depends on the current color:
const handleCubeClick = useCallback(
(mesh: THREE.Mesh) => {
const mat = mesh.material as THREE.MeshStandardMaterial;
mat.color.set(selectedColor);
setSelectedProduct(mesh.userData as Product);
},
[selectedColor]
);Step 5: useEffect → engine initialization
useEffect(() => {
if (!mountRef.current) return;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(/* ... */);
const renderer = new THREE.WebGLRenderer();
mountRef.current.appendChild(renderer.domElement);
}, []);This runs only once and initializes the full 3D engine: scene, camera, renderer, lights and cubes.
Step 6: interaction system (Raycaster)
const onMouseClick = (event: MouseEvent) => {
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(cubesRef.current);
if (hits.length > 0) {
handleCubeClick(hits[0].object as THREE.Mesh);
}
};This detects which cube is clicked by converting the 2D mouse position into a 3D ray.
Step 7: animation loop
const animate = () => {
requestAnimationFrame(animate);
cubesRef.current.forEach((cube) => {
cube.rotation.y += 0.01;
cube.position.x += dynamicOffset * 0.001;
});
renderer.render(scene, camera);
};It runs every frame, updates the 3D world and renders the scene continuously.
Step 8: reactive updates (useEffect)
Color sync:
useEffect(() => {
if (selectedMeshRef.current) {
(selectedMeshRef.current.material as THREE.MeshStandardMaterial).color.set(selectedColor);
}
}, [selectedColor]);Position update:
useEffect(() => {
cubesRef.current.forEach((cube, i) => {
cube.position.x = i * 2 - 4 + moveX;
});
}, [moveX]);🚀 3. What This Project Teaches
- React = UI controller
- Three.js = rendering engine
- Hooks = the bridge layer
⚡ 4. Which Hooks Matter Most in Real Projects?
🔥 Most important: useRef (engine core, critical), useEffect (lifecycle management, critical), useState (UI control, important).
🟡 Secondary: useMemo (performance optimization) and useCallback (event stability).
📌 Final Insight
In a React + Three.js architecture, React handles state, Three.js handles rendering, and hooks connect both worlds efficiently.
Full source code: react-learning-journey on GitHub — see the Canvas3D.tsx component.



