Interacting with objects with react-three-fiber

Adding some good looking objects to a scene using Three.JS becomes significantly more useful if the user is able to interact with them. In a typical Three.JS application that means you'd need to listen for mouse clicks in the screen space and use a raycaster to draw an imaginary line from the camera through the scene that calculates each object the line passes through in order to detect what the user has interacted with. Three.JS made that relatively simple, but react-three-fiber takes it a step further.

Objects in react-three-fiber have event props that work the same way as mouse listener callbacks in React - onPointerOver, onPointerOut, and onClick are all available and work almost exactly as they do in React when you're rendering HTML.

In the example scene above you can hover over the cube object and you can click on it. Hovering is achieved with two separate events - onPointerOver fires when the raycaster first 'sees' the object. onPointerOut fires when the raycaster can no longer see the object. In the example a useState variable is set to true when the onPointerOver event fires, and set to false when onPointerOut fires. This variable is used inside of a useFrame render loop to determine whether or not the geometry should be rotated.

The click event is simpler. If the raycaster sees the object when the user clicks the active value is toggled between true or false. active is used to choose between two colors in the material. This has the effect of turning the cube green when it's clicked, and back to red when it's clicked a second time.


const Box = (props) => {
  const boxRef = useRef();
  const [active, setActive] = useState(false);
  const [hover, setHover] = useState(false);

  useFrame(() => {
    if (hover) {
      boxRef.current.rotation.y += 0.05;
    }
  });

  return (
    <group ref={boxRef} position={props.position}>
      <mesh
        onClick={() => {
          setActive(!active);
        }}
        onPointerOver={() => {
          setHover(true);
        }}
        onPointerOut={() => {
          setHover(false);
        }}
      >
        <boxGeometry attach="geometry" />
        <meshLambertMaterial
          attach="material"
          color={active ? "green" : "red"}
        />
      </mesh>
    </group>
  );
};

Event propagation in r3f

Pointer events in react-three-fiber don't propagate in the same way that events in HTML do. In a page that's constructed from HTML an event will bubble up through the DOM. In react-three-fiber that doesn't happen, but events do propagate through the scene - if you have two objects lined up with the camera so that a pointer event would click on both of them then both click events will fire unless you call event.stopPropagation().