Build Your Own React - Part 3: State and Lifecycle

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 callingMVRDom.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.

City ∨Lines ∨Stations ∨

London11270

Show Logo

New York36468

Show Logo

Paris16303

Show Logo

Shanghai14364

Show Logo

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.

 

1

2

3

4

5

6

7

8

9

10

11

 

mountSimpleNode: (virtualElement, container, oldDomElement, parentComponent) => {

...

// add reference to domElement into component

let component = virtualElement.component;

while (component) {

component.setDomElement(newDomElement);

component = component.getChild();

}

...

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.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

 

handleComponentStateChange(component, nextState) {

component.updateState(nextState);

const nextElement = component.render();

nextElement.component = component.getRoot();

// start the normal diffing process here

const domElement = component.getDomElement();

const container = domElement.parentNode;

const childComponent = component.getChild();

if (childComponent) {

Reconciler.diffComponent(

nextElement,

childComponent,

container,

domElement,

component

);

} else {

Reconciler.diff(nextElement, container, domElement, component);

}

}

}

The setState() method in the Component superclass looks like this:

 

1

2

3

4

5

 

setState(newState) {

const prevState = this.state;

const nextState = Object.assign({}, prevState || {}, newState);

this.onStateChange(this, nextState);

}

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 inReconciler.mountComponent().

 

1

2

3

 

mountComponent: (virtualElement, container, oldDomElement, parentComponent) => {

const component = new virtualElement.type(virtualElement.props);

component.setStateCallback(Reconciler.handleComponentStateChange); // new line

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 callcomponentWillMount() 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: firstcomponentWillUpdate(), and finally componentDidUpdate().

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

 

handleComponentStateChange(component, nextState) {

const prevState = component.state;

if (component.shouldComponentUpdate(component.props, nextState)) {

component.componentWillUpdate(component.props, nextState);

// this part is the same as before

component.updateState(nextState);

const nextElement = component.render();

nextElement.component = component.getRoot();

const domElement = component.getDomElement();

const container = domElement.parentNode;

const childComponent = component.getChild();

if (childComponent) {

Reconciler.diffComponent(

nextElement,

childComponent,

container,

domElement,

component

);

} else {

Reconciler.diff(nextElement, container, domElement, component);

}

// finally we call componentDidUpdate

component.componentDidUpdate(component.props, prevState);

}

}

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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值