How does React Three Fiber actually work under
the hood? Maybe you have a great Three.js foundation but bringing this to React has
been challenging. Or maybe you're like me who came from the React background, but finds the
Three.js world a bit much to understand. This video was created for both of you. What I'm
going to do today is walk you through a side-by-side, one-to-one build of a Three.js project and
a React Three Fiber project at the same time. Sometimes the best way to understand a library
like React Three Fiber is to compare it to the OG, tried and true Three.js library so
you can see exactly what's happening on each side. We're going to start by creating a scene
on both the Three and the React Three Fiber side. Specifically we'll talk about how cameras
work, meshes and their geometries and materials, animation and lighting, and how all these
are implemented within JSX and how they actually work within React. We'll dive into the little
tricks that React Three Fiber does to make your renders look nice, including anti-aliasing,
tone mapping, and color output. And then we'll toss in a couple extras just for good measure.
All right so we got our two environments side by side here. On the left we have Three.js,
and on the right we have our React Three Fiber. We're going to start on the Three.js side
first just because there's a lot more involved in the setup, whereas React Three Fiber wraps
a lot of this for us. First we need to create a scene by creating a new Scene instance. For
those not familiar with Three.js, the entire library consists of classes that you can create
instances of. Next we have our camera. In this case we're going to use a PerspectiveCamera
with these defaults. These arguments refer to the field of view, the aspect ratio, the
near frustum, and the far frustum (which is talking about at what point does the scene
get clipped). These are the default values that React Three Fiber uses, so I'm just going to
put them in there so that we can have identical scenes. Next when you're building a Three.js
project, you're going to need to create a renderer. Three.js supports multiple renders, the most
common being the WebGL render. So we're going to use that. And by default it does not have
a transparent background, whereas React Three Fiber does. So we're going to turn alpha
to true to enable that, otherwise it would be a black canvas. We're going to set the renderer
to the window inner width and height, and then we're going to append it as a child on the
web page. This is a Next.js project, so that's why I'm using this element as my container.
And then finally in Three we need to have our render loop which is accomplished through
this pattern where you have a function (typically called animate), you call requestAnimationFrame()
from the browser, and then every frame it will render the scene. And with the request
animation frame - that will call animate() every frame available the browser provides us. And
then of course we need to call animate() at the very beginning, otherwise nothing kicks
it off. So this is the Three.js side. On the React Three Fiber side, as you can see much
simpler to start. We just have our Canvas. Note that this isn't an HTML5 canvas - this is a
special React Three Fiber Canvas that we're importing from their library. At this point
in time we have no children, so let's go ahead and get started by adding a mesh. So let's
come back on the Three.js side and we're going to add a mesh to the scene. In this case let's
add a cube. In Three.js your mesh needs to have a geometry
and a material in order for it to appear on the screen, so we're going to add a new geometry.
We're going to use a BoxGeometry so that it shows up as a cube. The parameters expect a
width, height, depth, and additional parameters. By default the width, height, and depth will
be 1 which works for us. And then we're also going to add a material. And for now we're
going to use a MeshStandardMaterial which has a default color of black. To add that to
the cube, we're just going to say cube.geometry equals geometry and we'll do the same with
our material. And then finally if we want to see the cube on the screen, we need to do a
scene.add() our cube. And when we save that, we don't see anything. Why don't we see anything?
Well by default our camera is at the 0, 0, 0 position. 0 x, 0 y, 0 z. And so is our cube,
so our cube and our camera are on top of each other. So what we're going to do is we're going
to move our camera back by setting its camera.position.z to 5, which is the default that React Three
Fiber sets it as. Now we can see our mesh! On the React Three Fiber side, we're going to go
ahead and do the same thing. So to add a mesh, it's a simple JSX element called mesh. And
then to add our geometry and material to it, they're simply going to be children of the
mesh. So in this case we're going to have a BoxGeometry and we're going to have a
MeshStandardMaterial. So you might be thinking, "Hold on. You just planned this perfect example,
but this doesn't really explain how any of this actually works. Like where is my scene
on the React Three Fiber side? How does the camera work I don't see that in here? We did
the rendering - where is the rendering?" And my response to that is, "You need to calm down!"
Do you know what channel you're on? Of course we're gonna go over that stuff. So let's start
with the scene. In Three.js, we have to create a new scene. And then at the bottom we are
rendering that scene within our animation loop. In React Three Fiber, this is all handled
for us within the canvas. Since React Three Fiber is trying to make Three.js accessible within
the React ecosystem, this is something that generally most people don't want to manage
themselves and don't need to manage themselves, so they abstract this for us. And React Three Fiber
does a pretty good job of choosing things that you normally don't really want to have
to worry about, but still giving you the option to tweak those things if you really
wanted to, which is best case scenario. So pretty sane defaults. So canvas is creating a scene
for us - what about the render loop? They're also doing that. So this animate() & request animation
frame - that's all being handled within this Canvas component. We don't need to worry about
that. Okay what about cameras? So cameras - again, we're explicitly creating that on the Three.js
side. On React Three Fiber our camera again, is being created for us within the Canvas. And
why is that? Why don't they require us to create the camera here ourselves? It feels
like maybe that could be a little bit too much magic happening. But I think it's a good
amount of magic, because if you come down here again to the animation loop, this render() function
explicitly depends on the scene and the camera. So if this camera was something that we added
to the scene ourselves, then that makes it complicated for React Three Fiber to also manage
this animation loop for us. So after understanding that, I think it's fair that the Canvas actually
manages our camera for us. And if you're wondering, "Well what if I want to manage the camera myself?"
No problem, there's actually a couple options. Number one, you can manage it through a prop
on the canvas. You can go "camera". And then this is props passed to that camera. So for example,
you could set the position. And by default we have 0, 0, and 5 which is matching
what we have here. Which is camera position "z" 5. If I save that nothing will change.
If I were to make that 10 and refresh this page, then you can see we move back further
in the z direction. And that behaved as we expected. So let's go ahead and actually just
remove this. You'll notice that the hot reloading doesn't apply for the camera. That just has
something to do with the way the props work. If you want to manage the camera as a JSX
element, you can. And you can do that by importing a special component called (you guess it)
PerspectiveCamera, but this one comes from the @react-three/drei package. React Three Drei is a
separate package by React Three Fiber that holds a bunch of goodies for React Three Fiber.
So React Three Fiber itself is just the minimal core to get Three.js working within React,
and then Drei provides a lot of extras to assist with it. So we just insert the perspective
camera here. We're going to add a field of view to make it match what we have on the
Three.js side. So that's 75. And then we need to give it a position as well to match the
Three.js side as well. The perspective camera component from Drei will default to just 0
for the position, so let's change that to 5. We also can't forget to say makeDefault true,
which will override the original default camera and make this camera now the active camera.
So we save that and then we can now change this to anything else and we can see it update
in real time. This prospective camera does work with hot reloading within React. So we're
going to get rid of this and go back to our original setup. So next let's talk about how
React Three Fiber treats Three.js elements within JSX. So starting with the Canvas, you
can basically treat the Canvas as the Scene in this context. Anything that's added to the
root level within the Canvas will be added to the Scene. So in this case we have our mesh
and just as we do in the Three.js side, we're adding our cube to the scene. That's the exact
same thing as what's happening here. This mesh is getting added to the scene. And then from
there any nested children will get added in the same way. For example, if I were to add
another mesh inside of this mesh - which is to be honest not that common - but if you're
to do that, then that would be equivalent to saying mesh.add() another mesh. So in this
case, say I had a cube and then I had a another mesh equals new Mesh(). Then I could say cube.add()
another mesh, and that would actually add another mesh as a child to cube. And you
can see that's equivalent of what's happening on the React Three Fiber side. Now in general
you're not going to be nesting mesh elements. You certainly can if you want to - more commonly
you'll probably see what's called a "group". And then you can have multiple meshes inside
of a group and then groups have their own position & rotation properties that can allow
you to move all its children as a whole. From the Three.js side this is not a new concept. You
would just create a group and then you'd say group.add() cube for example. So then how
does BoxGeometry and MeshStandardMaterial work? Since these are children of mesh, are
these actually getting added in the same way you say cube.add() geometry? And that's not
true, right? You don't add geometry as a child of cube - you set its geometry property and
its material property. So how come these two JSX elements get special treatment? How do
they actually work? And to answer that, you need to understand a pretty key concept in
React Three Fiber and it's called "attaching". So essentially React Three Fiber behaves like
this: any child of an element will be added with the .add() function, unless they have
a special property called "attach". If they have the property called "attach", then instead of
being added as a child of its parent, it will actually get added as that property of the
parent. So in this case, if I say attach geometry, that's saying, "Add this BoxGeometry to the
geometry property of the mesh." So in this case, you can see over here cube.geometry equals
geometry - that will actually attach it. Same with the material - I could say attach equals
material, and it will attach this mesh standard material to the material property in the same
way we are doing on the left hand side here. Now how come I didn't need to do that before?
Well for convenience, React Three Fiber automatically attaches all geometries that end with the
word "Geometry" to the geometry property, and all elements end with "Material" to the material
property. And that's just to make your life easy, because these are things that you'll
be adding all the time. The last thing to understand is the naming behind all the Three.js elements
within React Three Fiber. So you may have noticed that we followed this camel case (that has
a lowercase first character) within React Three Fiber, and then within Three.js we of course
follow the standard class-based pascal case (with the capital at the beginning). And so you
may be wondering, "Can I literally just one-to-one, for every Three.js class that exists, do the
equivalent within JSX, just with the difference of changing the first letter to lower case?"
And the answer is: Yes! That's basically correct. An important note when it comes to React Three
Fiber is that React Three Fiber actually knows very little about Three.js. And that might
sound kind of crazy, but the way React Three Fiber works is it actually just simply takes
this object and finds a Three.js equivalent, and then inserts it, right? It by default tries
to add children using the add() function, unless it sees the "attach". If you go and actually
look at the React Three Fiber renderer which is their actual reconciler code (the way they
actually convert JSX within React to something React understands), you can see that it's actually
very, very simple. And React Three Fiber at its core is just a very thin wrapper around
Three.js. And the reason why this is so important is because, especially for those who are coming
from the React side (people who understand React relatively well but they're trying to
dive into the Three.js world), you really need to understand that React Three Fiber isn't
doing anything special. It's literally just putting normal regular vanilla Three.js components
within the React ecosystem. But they're not creating anything special, unless of course
you're pulling additional components from the React Three Drei package - those are more or
less new things. But React Three Fiber itself is actually not doing anything special. And
this is really important when you're first getting started because if you're anything
like me who is somebody who started with React and really didn't know anything about Three.js,
and then tried to dive straight into Three.js within the React Three Fiber library, I almost
kind of expected React Three Fiber to give me all the documentation (give me all the instruction)
necessary to build a 3D world within React. And that's really the wrong approach. You want
to first and foremost understand how Three.js works, and then from there it's much easier
to build it within React, because all React Three Fiber doing again, is taking Three.js
concepts and making them work within a React ecosystem. But to actually build within the
Three.js world, you need to know Three.js first and foremost. And why did React Three Fiber
decide that these should be camel case instead of pascal case? AKA, how come the first character
is lowercase? Well that's convention for React. If you're familiar with React for the DOM,
all your div's, h1's, a-tag's - everything is lowercase by default. And that's the convention for for
any element you consider primitive, right? So React Three Fiber follows that exact same
convention - all the primitive components (in this case primitive with respect to Three.js),
are going to follow that first letter lowercase convention. So in summary React Three Fiber
JSX elements map one-to-one to their respective Three.js component, children within JSX elements
are added to its parent via the .add() function, unless they have a special "attach" method which
in that case gets added to the respective property that you chose, with geometry and
material nodes getting special treatment because you use them all the time. Because of this,
technically nothing stops you from using things like Vector3 in here if you wanted to. Now
I've never seen anyone actually do that. This doesn't make sense - you can't add a plain old
Vector3 to a mesh. But if you're familiar with Three.js, you can create vectors and you'd
say new Vector3() for example, which would consist of an x, y, and z. And I could easily add that
in here. And nothing stops me from adding that in here if I wanted to, of course I need to
have a reason for it. In this case this would not work because cube can't add a Vector3.
But hopefully this helps you understand a bit more about what's actually happening under
the hood when it comes to React Three Fiber. The last thing we need to talk about real
quick is arguments. So in Three.js I haven't been using arguments - I've just been using
the defaults. But if you can imagine we have our BoxGeometry, and the arguments that this
constructor expects are width, height, depth, and a bunch of other things. So let's say I
want to set my geometry to be 2 x 2 x 2. Let's save that. So our box is twice as big. How would I do
the equivalent on the React Three Fiber side? Well React Three Fiber has a special prop
called "args", and this will exist on nearly every single element. And this expects an array.
And this will literally get passed in as arguments to the constructor of the component that you're
trying to build. So in this case, I would say an array of 2, 2, 2 and under the hood literally
this is an array that's getting spread into the arguments (if you can imagine like this)
of the relative component. So if I save this, we can see it went ahead and changed the BoxGeometry
to be 2 width, 2 height, and 2 deep. It's not going to be long until you're going to
want to start componentizing your objects within the 3D scene. So on the Three.js side -
since we're using more or less vanilla JavaScript here, you can basically componentize your objects
however you like. Though one common pattern would be to use classes to do this. So for
example, we create a class called our Cube, and this would extend a mesh. And then within
the constructor we might create our geometries. Within React Three Fiber, this would of course
just be another React component. So let's talk about animation. In Three.js,
you might, let's say, rotate this cube in a circle by modifying its rotation within the
animation loop. Since we've abstracted this cube into a component,
we'd want to move this logic into that component. Typically you do this with a function called
update(). Then we just need to make sure we call update() on the cube within the animation loop.
Now we have an animating cube! Now how do we do the same thing in React? So React Three
Fiber gives us this nice hook called useFrame(). And you can think of this useFrame() hook as
behaving almost exactly like our update() function on the cube. Every single frame this function
is going to get called, and you can do whatever you like with it. So in React we need to somehow
get access to this mesh in order to modify its rotation x and y like we've done in Three.js.
To do that, we can use refs. Perfect, now we have a rotating cube in React
as well. So there's two things we need to talk about here: So first off, what really is a ref
with context to Three.js? Well in React Three Fiber, the ref on each JSX element is literally
going to point to the exact instance created within Three.js. So attaching a ref on this
mesh is literally going to point to the exact Three.js mesh instance. The same thing would
apply if I were to add a ref to the geometry or the material - that would point to an instance
of that geometry within Three.js. If you've used refs within regular old React on the
DOM, it behaves the exact same way. If you add a ref to a div, you literally have a reference
to the instance of that div within the DOM. Okay second thing to point out is if you're
OCD like me, the left-hand side Three.js is probably driving you crazy right now. Why is
that looking so pixelated whereas the React Three Fiber is looking really smooth? Don't
worry, we're going to talk about this in just a little bit. By the way, there are other ways
to animate within React Three Fiber, specifically there's a library called React Spring which
integrates seamlessly with React Three Fiber. React Spring essentially allows you to create
animations in a much more declarative way and is actually cross platform - works in the
DOM, works on React Native, and works in Three of course. But we'll save that for another
video. Before we add lighting to the scene, I want to bring up one last point and that
is around disposing. If you've used Three.js before, you might be familiar with disposing
where Three.js actually requires you to manually dispose your geometry and material for example.
And the reason for that is because usually you would cache these and Three.js doesn't
know when you are done with them. So one thing you might do is add a dispose() function and
then you would dispose, for example, your geometry here and anything else that you need to dispose.
And then at the time that you want to get rid of your cube, you call that cube.dispose().
Within React we actually don't need to worry about this at all, and that's because React
actually already understands the exact life cycle of all the different components within
React. So React Three Fiber takes advantage of this and actually will automatically call
dispose for you when elements are removed from React. Pretty handy. All right so let's
add some light. An ambient light in Three.js is a light that
is spread evenly across all objects. Let's do the same in React. Let's also add a point
light so that we can add a little bit more dimension to the scene. Point lights exist within a very specific
position on the scene. Now that didn't seem to do anything - we'll come back to that in
a second. Let's add on the React side. And now we can see (at least on the React side)
we're actually getting a bit of dimension here. Okay so why is the React version looking
so much better than Three.js version? So now is a great time to talk about the rendering
appearance. And there's a number of different ways we can configure how Three.js will appear
when rendered. So first of all let's address this pixelated look. You can fix that by adding
another property to the WebGL renderer called anti-aliasing. This actually isn't on by default
within Three.js, however it is within React Three Fiber. So if I save that we're already
looking a little bit better. Still not perfect though - it looks a little bit fuzzy on the
edges here, where it's quite crisp here. And this has to do with the pixel ratio. So by
default, Three.js will render at a pixel ratio of 1, whereas React Three Fiber will try
to match what your browser supports. So we'll have to manually do that within Three.js. So that looks a lot crisper now. We still
have a flat white looking box compared to the 3D one on the React side, and this has
to do with tone mapping. And by default React Three Fiber chooses to use a ACES Filmic tone
mapping which we'll use here. Okay so finally we have a 3D looking object on the Three.js
side, but it still looks a little darker than the React Three Fiber side. And it has to do
with the color space that Three.js is outputting. We can fix that by setting the output encoding
to the sRGB encoding. And now our cubes are looking identical. So why does React Three
Fiber have different defaults than Three.js? Well React Three Fiber is making the decision
here that they want to provide defaults that they think the majority people will want. And setting
this tone mapping and output encoding to these values achieves that. Of course nothing stops
you from changing these if you want to. Within React Three Fiber, you can get access to the
GL renderer through the "gl" prop on the canvas and you can set your configuration there. Of
course our cube would not be complete without adding some color, so let's make our cube blue.
In Three.js we can do this by setting the color on our material. So we could go ahead and set
the color within the constructor, or alternatively we can set the color on the material itself.
In React Three Fiber it's done similarly, where color would be a prop on the MeshStandardMaterial
element. Or if you wanted to be absolutely crazy and do something absurd, you could actually
add a color as an element within MeshStandardMaterial, and use our "attach" prop to attach
this color to the MeshStandardMaterial color property. Please don't do this - I see almost
no value of making this an actual child of MeshStandardMaterial, but I'm just trying to
really demonstrate what kind of options are available within React Three Fiber once you
understand a little bit more how the JSX elements work. Okay last thing we never addressed here
is how this array of three values (also known as a triplet) actually works. From the Three.js
side of the world, you don't really see this convention that much. So how come we're using
that all the time within React Three Fiber? And to answer that you need to understand
how something like position actually works within Three.js. So when I create a new point
light for example, it's going to have a position. This would also be true for most any other
thing within the scene (like your mesh would have a position, groups do, etc). So looking back
at our point light, if I hover over position you can see that this is a Vector3. Now I
never created this position - as soon as I create a new point light, that position is automatically
added to the point light for me. And under the hood what's actually happening there is -
this position - we're creating a new instance of a Vector3 (which we talked about before
consists of an "x", "y", and "z" value). Another thing worth noting here is I can't just assign a
new Vector3 to my position. As you can see here, position is a read-only
value and cannot be reassigned. So the way that you actually would change your position
is instead of overriding the Vector3, you would just call set() on that Vector3. And there's
good reason why this is the case - Vector3's aren't necessarily cheap to create. You can
imagine Vector3's are used all the time within Three.js. All the positions need Vector3's,
and many other things. So if every time we want to move an object around we had to create
a brand new Vector3, that would be extremely expensive. So instead we're just mutating that
Vector3 by calling set(). So how does this translate to the React Three Fiber world? Well if we
look at the type of position here we can see that it's a Vector3, which is interesting. If
we actually drill into that we can see that this is actually a special Vector3 wrapped
by React Three Fiber that allows you to input a number, an actual Vector3, or this triplet
notation. So technically I actually could put a new Vector3 in here with my values and this
would work. Now this is an anti-pattern within React Three Fiber for the exact same reason
we just talked about. Within React, you should know that the code within this functional
component will be called every render and if we are creating a new Vector3 every single
render, this is going to be extremely expensive and not performant at all to run. So instead,
best practice would be to do what we did originally, which is this triplet notation. And an array
of three values is relatively cheap to recreate every render. But this still feels a little
bit like magic - how is this triplet of three values getting passed into my point light's
position? Well almost like the args, what's happening here is this array of three values
is actually getting spread into the set() function of position. So what React Three Fiber has
done here is they do allow you use Vector3's (and if you cache them properly that's reasonable),
but in order to take advantage of the declarative nature of React and allowing you to be changing
the values within JSX, pretty much every property that's available on these objects allow you
to use these different notations. And specifically this triplet notation you can use, which will
always be spread into the set() function of that respective prop. So there you have it,
Three.js and React Three Fiber side by side. I hope this video has helped you really understand
what's happening in React Three Fiber. I really enjoy making these videos, but they do take
a lot of time and effort. So if you found this video useful and would like to see more, liking
the video and subscribing would no doubt help with that. Have a great day and I'll catch
you down the next rabbit hole!