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
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
render(), which is always overridden.
As you can see, its constructor takes the props as an argument and assigns them
this.props, which is why, when you override the constructor, you have to
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.
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
is a function. Remember that, when rendering a component, the first argument to
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:
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
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
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:
mountSimpleNode(). We move the code we wrote for
mountElement() in part
mountSimpleNode() and create a new method
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
render() on the component which returns a new virtual element. Before
Reconciler.diff, we store a reference to the component in the
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...
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
diffComponent() to handle nested
components. But first, let's add a couple of methods to our
Component superclass, namely
diffComponent(), we have to check whether the component has child components
, and if it does, call
diffComponent() recursively until we get a childless
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
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
- 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.