In this part, we'll add state, i.e. setState()
method, to our framework.
We can already build stateful applications by passing down props and calling
MVRDom.render()
each time the state changes. However, one of the main features
of React is component state. Component state is a way of triggering updates in
just one individual component and its children.
A typical use case for setState()
is hiding and displaying certain elements of
a component. Below is an app that displays some stats about four big metro
systems. You can show and hide the logo for each metro system by clicking the
buttons. Clicking the headers will sort the rows. All state transitions in the
app are handled through setState()
, running on Minimum Viable React as it
stands at the end of this article.
You may have noticed that the application has a bug. The bug has to do with the lack of key attributes in the framework. Don't worry about that now, it's the topic for part 4.
State
This is, unfortunately, where things start to get a bit messy. In the past, we've had a clean tree structure where DOM nodes have pointers to virtual elements, and virtual-elements have pointers to components. Furthermore, components know nothing about the reconciler. This has to change because we have to give components the ability to notify the framework that their state has changed and that they should be re-rendered.
First, let's add the DOM element reference to components. We
can do this in Reconciler.mountSimpleNode
. Note that we add the reference to
the parent component and all of its children.
|
|
Here's a visualization of the references between Components, Virtual Elements, and DOM Elements.
In React, setState()
is asynchronous, i.e. the component won't be re-rendered
immediately. I think this is mostly a performance optimization enabling batching
of updates. In MVR, we opt for simplicity by making everything synchronous.
Next, let's add a method called handleComponentStateChange()
to
Reconciler
. It's pretty straight-forward: we'll update the state of the
component, re-render it, and then start the normal diffing process. Only this
time, diffing starts at the component whose state was changed, which is why
we need a reference to the DOM element associated with the component.
|
|
The setState()
method in the Component
superclass looks like this:
|
|
this.onStateChange()
is the handleComponentStateChange()
method from above.
We just need a way to pass this function to Component
as a callback. We can't
do it in the constructor because that only takes one argument: props. We'll do
the next best thing, which is to set it right after initialization in
Reconciler.mountComponent()
.
|
|
Lifecycle methods
Now that we have implemented the whole lifecycle of components (mounting,
updating, unmounting), it's fairly trivial to add the lifecycle methods. All we
have to do is add all of them to the Component
superclass and them find the
right place in the code to invoke each method. The methods are:
- componentWillMount
- componentDidMount
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- componentDidUpdate
- componentWillUnmount
I won't go through each one as, hopefully, it's quite easy to see how you'd call
componentWillMount()
in Reconciler.mountComponent
and so on. Let's, however,
take a look at how we would change the handleComponentStateChange()
method
above.
The biggest change is adding a conditional. Before each update, we have to call
shouldComponentUpdate()
with the next props and state. In this case, the props
don't change so we just pass in the current props and the next state. We'll also
have to add in the other lifecycle methods in a logical order: first
componentWillUpdate()
, and finally componentDidUpdate()
.
|
|
Conclusion
We're almost done! Adding setState()
was a bit messy as we had to add
complexity to the simple tree structure from the previous articles. Lifecycle
methods, however, are almost trivial. The problem is merely to find the correct
spot in the code for invoking them.
There's one important feature left: keys. Currently our only criterion for whether a component should be updated or replaced is whether they have the same constructor. This is not enough. We need each element to have some kind of an identity so that we can determine weather two elements should be considered the same instance of a component. We'll tackle this problem in part 4.