
When Your UI Needs to Break the Flat Screen: React and 3D with React Three Fiber
We've been building UIs on flat screens for decades. But what happens when you need something more? When data visualization demands depth, or an interactive product showcase needs a real sense of presence? React Three Fiber is the tool that lets you bring the full power of Three.js into your React a
by Sunil Band
Beyond the 2D Canvas
For most of my career, frontend development has been about making pixels dance on a flat screen. Divs, spans, SVGs – all powerful, but fundamentally constrained to two dimensions. Then you hit a wall: a client wants a compelling data visualization that feels immersive, a product team needs an interactive 3D model, or a designer envisions a UI element that literally pops out at the user. You can hack around it with CSS transforms, sure, but it's always a compromise. You're fighting the browser, not working with it.
This is where React Three Fiber (R3F) enters the picture. It's not just a wrapper for Three.js; it's a renderer for Three.js that speaks React. This distinction is crucial. It means you're writing declarative React components, leveraging hooks, context, and all the familiar patterns, but those components are rendering directly into a WebGL canvas via Three.js. No more imperative DOM manipulation to manage your 3D scene; it's all React.
Why declarative 3D matters
If you've ever wrestled with raw Three.js, you know it's a powerful beast. But it's also highly imperative: create a scene, add a camera, create a mesh, add it to the scene, render. Managing updates, state, and complex interactions in a large Three.js application can quickly become a tangled mess. You end up with bespoke state management and event systems that feel like reinventing the wheel.
React's declarative nature and component model are a perfect fit for this. You describe what your 3D scene should look like based on your application state, and R3F, powered by Three.js, handles the how. Need to show or hide a model? Conditional rendering. Change its color? Pass a prop. Respond to a click? An onClick handler on your 3D mesh. It feels like regular React, but in three dimensions.
Getting Started with React Three Fiber
Let's build a simple scene: a rotating cube. This is the 'hello world' of 3D, but it demonstrates the core principles of R3F beautifully.
First, you need a canvas. Not a <canvas> HTML element, but R3F's <Canvas /> component. This component sets up the WebGL context, the scene, and the camera for you. You don't have to worry about new THREE.Scene(), new THREE.PerspectiveCamera(), or new THREE.WebGLRenderer(). It's all abstracted away.
import React, { useRef } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { Mesh } from 'three'; // Import Three.js types if using TypeScript
// A React component that represents our 3D box
function Box() {
// This reference will give us direct access to the mesh object
const meshRef = useRef<Mesh>(null); // Use a ref to access the underlying Three.js mesh
// Subscribe this component to the render-loop
useFrame((state, delta) => {
if (meshRef.current) {
meshRef.current.rotation.x += delta; // Rotate the cube every frame
meshRef.current.rotation.y += delta; // delta is the time since last frame
}
});
// Return the view, these are regular Three.js elements expressed in JSX
return (
<mesh ref={meshRef} scale={1}>
<boxGeometry args={[1, 1, 1]} /> {/* Define the shape of the cube */}
<meshStandardMaterial color="hotpink" /> {/* Define the material and color */}
</mesh>
);
}
export default function App() {
return (
<div style={{ height: '100vh', width: '100vw' }}>
<Canvas camera={{ position: [0, 0, 5] }}> {/* Set up the canvas with a camera position */}
<ambientLight intensity={0.5} /> {/* Add some basic ambient lighting */}
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} /> {/* Add a spot light */}
<pointLight position={[-10, -10, -10]} /> {/* Add a point light */}
<Box /> {/* Render our custom Box component */}
</Canvas>
</div>
);
}In this example, the <Box /> component is a functional React component. Inside it, we use useRef to get a reference to the actual THREE.Mesh object that R3F creates. The useFrame hook is where the magic happens for animation. It subscribes to R3F's render loop, giving us access to the current state and the delta time since the last frame. We use delta to ensure smooth, frame-rate independent rotation.
The JSX within <mesh> isn't HTML; it's a declarative way to define Three.js objects. <boxGeometry /> and <meshStandardMaterial /> are THREE.BoxGeometry and THREE.MeshStandardMaterial instances, automatically created and managed by R3F. You pass props to these JSX elements, and R3F translates them into Three.js property assignments.
Interacting with 3D Elements
One of the biggest advantages of R3F is how seamlessly it integrates with React's event system. You don't need to write complex raycasting logic to detect clicks or hovers on 3D objects. R3F handles that for you, exposing standard DOM events directly on your 3D components.
Let's make our cube clickable and change its color on click, then scale it up on hover.
import React, { useRef, useState } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { Mesh } from 'three';
function InteractiveBox() {
const meshRef = useRef<Mesh>(null);
const [hovered, setHover] = useState(false);
const [active, setActive] = useState(false);
useFrame((state, delta) => {
if (meshRef.current) {
// Scale based on active state, using a simple animation for smooth transitions
meshRef.current.scale.set(
active ? 1.5 : hovered ? 1.2 : 1,
active ? 1.5 : hovered ? 1.2 : 1,
active ? 1.5 : hovered ? 1.2 : 1
);
meshRef.current.rotation.x += delta; // Still rotating!
meshRef.current.rotation.y += delta;
}
});
return (
<mesh
ref={meshRef}
onClick={() => setActive(!active)} // Standard React onClick handler
onPointerOver={() => setHover(true)} // onPointerOver for hover detection
onPointerOut={() => setHover(false)} // onPointerOut to reset hover state
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={active ? 'orange' : hovered ? 'lightgreen' : 'hotpink'} />
</mesh>
);
}
export default function App() {
return (
<div style={{ height: '100vh', width: '100vw' }}>
<Canvas camera={{ position: [0, 0, 5] }}>
<ambientLight intensity={0.5} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} />
<pointLight position={[-10, -10, -10]} />
<InteractiveBox />
</Canvas>
</div>
);
}Notice how onClick, onPointerOver, and onPointerOut are just like their DOM counterparts. R3F handles the underlying raycasting and event dispatching, making it feel completely native to React development. This is a game-changer for building truly interactive 3D experiences without the boilerplate.
The Ecosystem: Drei
While R3F gives you the core renderer, the Drei library (@react-three/drei) is an essential companion. Drei is a collection of useful helpers and abstractions built on top of R3F. It's like a component library for your 3D scenes. Need orbit controls? A text label? A responsive image plane? Chances are Drei has a pre-built component for it.
For instance, setting up orbit controls in raw Three.js is a few lines of imperative code. With Drei, it's a single component:
import React, { useRef, useState } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei'; // Import OrbitControls from Drei
import { Mesh } from 'three';
function InteractiveBox() { /* ... (same as before) ... */ }
export default function App() {
return (
<div style={{ height: '100vh', width: '100vw' }}>
<Canvas camera={{ position: [0, 0, 5] }}>
<ambientLight intensity={0.5} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} />
<pointLight position={[-10, -10, -10]} />
<InteractiveBox />
<OrbitControls /> {/* Add orbit controls to enable camera rotation/zoom */}
</Canvas>
</div>
);
}Just adding <OrbitControls /> gives you a fully functional camera controller. This drastically speeds up development and helps maintain a cleaner codebase, as you're composing functionality rather than reimplementing it.
Performance and Trade-offs
Anytime you're dealing with 3D graphics, performance is a primary concern. Three.js and WebGL are powerful, but they demand respect. While R3F makes development easier, it doesn't magically solve performance issues stemming from overly complex scenes, unoptimized models, or excessive draw calls.
Consider these trade-offs and best practices:
- Understand the Three.js primitives: While R3F abstracts a lot, having a basic understanding of Three.js concepts like geometries, materials, meshes, and lights is essential for debugging and optimization. R3F is a thin layer; the underlying principles of 3D rendering still apply.
- Optimize your assets: Large 3D models (GLB/GLTF) with high poly counts or unoptimized textures will inevitably slow down your application. Use tools like Blender or online optimizers to reduce file sizes and complexity.
- Batching and Instancing: For scenes with many identical objects, Three.js (and by extension, R3F) offers techniques like instanced meshes to draw them efficiently. Drei often provides helpers for this. Don't render 1000 individual
<mesh>components if you can instance them. - Leverage
useMemoanduseCallback: Just like in regular React, prevent unnecessary re-renders of complex 3D objects or expensive calculations. If a geometry or material doesn't change, memoize its creation. - GPU vs. CPU: Most of the heavy lifting in 3D is on the GPU. Be mindful of JavaScript-side calculations in your
useFrameloops, as these run on the CPU and can become a bottleneck if not optimized.
R3F's strength is its React-first approach, which can sometimes lead to developers forgetting they're still doing 3D. The abstractions are great, but they're not a magic bullet for performance.
Beyond the Cube: Real-world Applications
The rotating cube is a classic for a reason, but R3F shines in much more complex scenarios:
- Interactive Product Configurators: Imagine customizing a car with different paint jobs, wheel options, and interiors, all rendered in real-time 3D within your web app.
- Data Visualization: Representing complex datasets in three dimensions can reveal patterns that are invisible in 2D charts. Think geographical data, network graphs, or scientific simulations.
- Architectural Walkthroughs: Immersive experiences for real estate or design previews.
- Creative UIs and Games: Non-standard navigation, dynamic backgrounds, or even simple browser-based games.
The beauty is that you can integrate these 3D experiences directly alongside your existing 2D React components. You might have a 3D product viewer taking up half the screen, with traditional React forms and buttons handling configuration on the other half. It's a cohesive experience.
Wrapping up
React Three Fiber fundamentally changes how you approach 3D on the web. It takes the power of Three.js and combines it with the developer experience of React, letting you build complex, interactive 3D scenes with familiar patterns. You're no longer battling imperative APIs; you're composing 3D experiences like any other React component.
If you've been hesitant to dive into WebGL and 3D because of the perceived complexity, R3F is your gateway. Start by cloning a simple R3F boilerplate (like the one available on the R3F documentation or a CodeSandbox template) and try replacing the default scene with your own ideas. Play with different geometries, materials, and lights. Then, incrementally introduce components from @react-three/drei to see how quickly you can build more sophisticated interactions.
Your next UI doesn't have to be flat. It's time to add another dimension to your React toolkit.

When Your State Management Needs to Stop Thinking in Actions and Start Mutating
Many state management libraries force you into an 'actions and reducers' pattern. While powerful, it often adds unnecessary boilerplate for simple updates. Sometimes, you just need to directly modify state, and mutators offer a more ergonomic and intuitive approach, especially for deeply nested data

When Next.js Cache Components Refuse to Build Your App
Next.js 16.3 introduced 'Cache Components' to optimize server-side rendering, but getting them to work can be a headache. I spent a frustrating afternoon debugging why a simple page wouldn't build, only to uncover some subtle yet critical design considerations. It turns out, this feature forces you

Vite: When Your Dev Server Needs to Be as Fast as Your Code
We've all been there: waiting for a dev server to spin up, or watching HMR take precious seconds to reflect a simple CSS change. It breaks flow, kills productivity, and makes you wonder if you should just switch to a static HTML file. Vite changed that for me, fundamentally altering how I think abou


















