Understanding React Hooks in a Real Three.js Project (Practical Guide)

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:

  1. The general React Hooks concept
  2. Real usage inside a Three.js project, step by step
  3. 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.