In this article, we pick up where we left off in part 1. Our first app isn't particularly impressive, but it works well and has some features crucial to a practical React implementation. For example, it supports event handlers and updates only the parts of the DOM that change.
Let's face it, React wouldn't be a popular framework if it weren't for its composable component system. That's what we'll implement in this edition of the Minimum Viable React.
Let's start by looking at how we use components in React. Here's a simple
example. First, we create a new component by extending React.Component
class.
|
|
Next, we call createElement()
with that class.
|
|
The first obvious thing we have to do is create an MVR.Component that we can
extend. Let's do that in MVR.js. Here's the code we'll add at this point. We'll
add other methods to this in the future, but for now, we'll only need
constructor()
and render()
, which is always overridden.
|
|
As you can see, its constructor takes the props as an argument and assigns them
to this.props
, which is why, when you override the constructor, you have to
call super(props)
.
The Big Picture
The difference between components and simple virtual elements is that components don't directly represent a real DOM element. Instead, they are factories that create more simple virtual elements that will eventually be used to create DOM elements. We need a way to keep track of which component is responsible for creating which simple element.
Because our tree of virtual elements no longer matches the DOM exactly, we'll have to update the diagram from part 1.
Now each virtual element stores a reference to the component that created it. This way, as we're diffing the DOM, we can re-render components with new props to create the new virtual elements, which we'll go on and diff with the real DOM. If this sounds confusing, some code will hopefully make it easier to wrap your head around.
MVRDom.js
Let's look at MVRDom.js and Reconciler.diff()
. The new code in this
function is on lines 3-7. On line 5, we test if the type of the element.type
is a function. Remember that, when rendering a component, the first argument to
MVR.createElement
is the component class. The type of a class in Javascript is
function
. Therefore, we can detect a virtual element that represents a
component by checking if its type is function
. If the check returns true, we
invoke a new method: Reconciler.diffComponent()
.
|
|
Next, let's inspect Reconciler.diffComponent()
. It takes the new virtual
element, an optional old component, the container, and the DOM element. Now,
remember that with components, the virtualElement.type
refers to the constructor of
the component. To know if the new component will be of the same type as the
previous one, we can compare the new constructor to the old constructor.
|
|
Updating a component
If the constructors are the same, we can update the component. This involves
updating the props and re-rendering the component. Let's add the updateProps()
method
to the Component
superclass.
|
|
After updating the props, we call render()
on the component, which returns a
new virtual element that represents an actual DOM element (it could be another
component too, but we'll handle this edge case later). Then we continue diffing
by calling Reconciler.diff()
with the new virtual element.
Mounting a component
Let's first see how we mount a component into the DOM. First, we split the
Reconcile.mountElement()
to two functions: mountComponent()
and
mountSimpleNode()
. We move the code we wrote for mountElement()
in part
1 to mountSimpleNode()
and create a new method
mountComponent()
.
|
|
mountComponent()
is very straight-forward. We create a new component from the
constructor, which, I hope you remember, is stored in element.type
. Then we
call render()
on the component which returns a new virtual element. Before
continuing with Reconciler.diff
, we store a reference to the component in the
virtual element.
|
|
That was easy! Now we have a framework that can handle components as well as simple elements. We're done with this part, right? Well, not quite...
Nested Components
There's an annoying edge case we have to handle. Sometimes, a component doesn't
return a simple element but another component. For example, we might have a
MessageContainer
component that renders a Message
component. MVR must
support an arbitrary degree of nesting like this.
|
|
This could be solved in various ways, but in MVR the solution is to store a reference to the parent component into the first virtual element. In other words, we form a linked list of sorts, where the first item is the parent component. The diagram below will make this clearer.
Now, let's update mountComponent()
and diffComponent()
to handle nested
components. But first, let's add a couple of methods to our Component
superclass, namely getChild()
and setChild()
.
|
|
In diffComponent()
, we have to check whether the component has child components
, and if it does, call diffComponent()
recursively until we get a childless
component.
|
|
In mountComponent()
, we check if the virtual element returned by render()
is a
component, and if it is, call mountComponent()
recursively until we get a simple
element. We also have to set up the linked list I mentioned earlier using setChild()
.
|
|
Conclusion
That's it! MVR now has support for components. We can already build pretty complicated apps with this framework, but a few critical things are missing, namely:
- Lifecycle hooks
setState()
- Better list reconciliation (using keys)
We'll tackle items 1 and 2 in the next edition of Minimum-Viable-React, and in the 4th and last part we'll add a better list reconciliation algorithm. I hope you learned something from this and remember to leave a comment if you have any feedback or questions.