Why did React render your component?

Tl;dr

  1. How does react element look like?
  2. What is rendering?
  3. Initial render v/s Reconciliation.
  4. What are render and commit phases?
  5. When does react execute side effects?
  6. What is the criteria for react to render your component?
  7. Brainstorming query


1. How does react element look like?

Once we have imported React in our JS file, React provides a method named createElement() to create react element e.g. is 1.(b),

Print React element
1.(a)
1.(b)

We can notice in 1.(a) that it takes element type as first argument i.e., "div", attributes and values of element as key-value pairs in second argument i.e., { id: "parent"} and third argument is array of children react elements of "div" (first argument). Important keys of object logged at console are - type (denotes type of element) and props (denotes attributes + children).

{
type: Object<div>,
props: {
   id: "parent", // first attribute and its value
   children: [ // nested children which are themselves react elements
      { type: Object<div>, props: { } }, ...
   ]
}
}        

Going forward in this article whenever we say react element please assume we refer to above object. In above code, parent variable holds JSX and JSX is react element i.e., above JS object. Hence, JSX === react element === above JS object.


2. What is rendering?

y = f(x)

where, f() = static component code, x = data like, props, state, context, etc. and y = UI look when data is combined with component code. Here, component code is static but data makes component dynamic. This data can be state, props, etc. or it can be rules of a configurable UI. Suppose, f1(x1) is parent of f2(x2). If we want to have multiple f2(x2) inside f1(x1) we can manipulate data x1 accordingly. E.g., f2(x2) is image and x2 is image url and f1(x1) needs to have multiple images then initialising x1 as an array could be one solution.

Rendering a component means calling f(x) for some x i.e., calling a component with some data. We could check whether component rendered or not by adding a console.log("Rendered") inside function component e.g., 2.(a):-

2.(a)


3. Initial render v/s Reconciliation


3.1 Initial render

When react renders our App component the first time under root, that render will be initial render. E.g., we start server using npm run start now this will create a server and react does initial render. Browser does not understand JSX because JS engine works only upon vanilla JS. So, bundler recursively traverses down the child component and convert all the components' JSX to react element i.e., JS object and links them to their parent.

The structure formed due to this is fiber tree e.g., 3.1.(a). Each node of fiber tree is fiber element itself. To link each react element (JS object) to its parent, bundler uses pointers to memory references of JS objects. Fiber tree is later converted to render tree and render tree is stored in RAM allocated to browser by OS. The size of render tree contributes to bundle size hence we can say that fiber tree's size is directly proportional to bundle size. Fiber tree is just collection of fiber elements and fiber element can be seen by printing element on console while writing unit test cases using react testing library e.g., 3.1.(b) and 3.1.(c)

3.1.(a)


3.1.(b)


3.1.(c)

During initial rendering, react constructs fiber tree. During the construction of fiber tree it has to recursively traverse to every component and constructs its fiber element. To construct fiber element it transpiles react element to JS object and links them to their parent. While doing all these processes, it has to call every component and hence in initial rendering every component is rendered. We can check this by adding console.log("Rendered") to every component including parent and child.


3.2 Reconciliation

Reconciliation is the process of recursively traversing the fiber tree. When state in a component updates react triggers a reconciliation from top of current fiber tree and generates a work in progress (WIP) fiber tree by cloning fiber nodes with mutated data. Entire process of reconciliation happens in 2 phases - render and commit. Common cases when reconciliation is triggered are setState, custom hook, etc.


4. What are render and commit phases?

Lets take below example to understand how react performs reconciliation:-

Given: We have a Parent component which contains child components: A and B. A is parent of component C. All components have useEffect or componentDidMount. Component A triggers setState. listOfEffects = [ ]. All list of effects will be stored in an array but initially this array will be empty.


4.(a)

4.1 Render phase

Reconciliation process:-

  1. As soon as the state update is triggered in component A, react starts the reconciliation (recursively) pass from top of fiber tree. Usually, top will be root but in our example it is Parent component.
  2. React checks whether the state which was triggered to be updated is defined in Parent or not (i.e., from which component setState is called). It is not defined in Parent so skips it and moves to its first child which is A and finds that A triggered state update.
  3. For component A, react will call beginWork. React will go inside A, clones the current fiber node A as A' and mutates cloned node A' with new state. This cloned node is part of WIP fiber tree. Then, react checks whether it should call beginWork for A's children i.e., beginWork should only be called after checking these criteria (its covered in sections 6). Calling beginWork for component actually renders the component. Similarly, triggering state update is one of those criteria that's why beginWork was not called for Parent component and Parent was not rendered.
  4. We here assume component C does not meet the criteria hence react calls beginWork for C as well and creates cloned fiber node C'. After that, react finds C does not have any children so it terminates by calling completeWork and control moves to completeWork. At this point, callback of useEffect of C will be pushed to array listOfEffects = [ useEffect of C ].
  5. After C, recursive code will move back to A and call completeWork for A. Also pushes useEffect of A to array listOfEffects = [ useEffect of C, useEffect of A ]. Now, react will move recursively to Parent and sees there is one more child B. Hence, it repeats the same process for fiber sub-tree of B. Final array listOfEffects = [ useEffect of C, useEffect of A, useEffect of B, useEffect of Parent ].
  6. Once completeWork for Parent finished execution, it means by now every node of current fiber tree is cloned, hence it will break pointer references between current fiber tree's nodes and their corresponding cloned nodes. Newly obtained collection of cloned nodes is WIP fiber tree and its separate from current fiber tree. Current fiber tree is one which is currently displayed on UI and stored as render tree in RAM.
  7. Entire above process is called render phase under reconciliation (or hydration phase in react official repository code terminology btw this is just what I thought after reading some of their code as they have used word "hydration" excessively).


4.2 Commit phase

  1. listOfEffects = [ useEffect of C, useEffect of A, useEffect of B, useEffect of Parent ] is a result of successful completion of render phase. In commit phase, react swaps pointers of current fiber tree and WIP fiber tree under root.
  2. Also, in commit phase listOfEffects is iterated in same order and callbacks are stored in memory. From memory, callbacks enter to callback queue. Event loops checks whether call stack is empty or not. Callbacks of useEffect enter into call stack which quickly executes them. Order will be -> useEffect of C, useEffect of A, useEffect of B, useEffect of Parent.
  3. This process is called commit phase under reconciliation.


4.3 Example code

4.3.(a)


4.3.(b)


5. When does react execute side effects?

Side effects like, componentDidMount, useEffect, etc. are executed in the commit phase. But, their order of execution is determined in the render phase. So whenever there is problem in what order these side effects execute you could navigate through reconciliation process and confirm their order of execution.


6. What is the criteria for react to render your component?

React uses shallow comparison a lot in their code base. Criteria for react to render your component are here. React won't render the component if control is able to reach lines here. In order to reach attemptEarlyBailoutIfNoScheduledUpdate, all the following criteria need to be true:-

  • current !== null i.e., it is not initial render and component is already mounted.
  • oldProps === newProps i.e., no change in props based on shallow comparison.
  • hasLegacyContextChanged === false i.e., context provider is not rendered because there is no change to context value.
  • hasScheduledUpdateOrContext === false i.e., there is not setState type of update either through custom hook or directly.

In below code, when Parent renders react also renders component A. This is because bundler transpiles A to react element and JS object like, { type: A, props: { } }. With every render of Parent, react encounters A and recreates its props object as newProps. Now, when react shallow compares oldProps === newProps, it fails and hence one criteria is not met and A is rendered.

We can use React.memo and its benefit is it shallow compares every properties of oldProps and newProps. Instead of doing this: oldProps === newProps, it does: Object.keys(oldProps).some(key => oldProps[key] !== newProps[key])

6.(a)
6.(b)


6.1 hasLegacyContextChanged === false

In below code, output is: Parent rendered, A rendered, B rendered, C rendered. This is because when Parent renders, A is called which creates new props object for B which is referentially different from previous props object hence B is rendered and similarly, C is rendered.

6.1.(a)

In below code, output is: Parent rendered, C rendered. This is because when Parent renders, A is not rendered as its passed as children so no new props will be created for A. But, reconciliation pass will go to its children B then it will skip it but since C consumes context its rendered.

Also, if we change contextValue to primitive value like number, etc. then C won't render because Context.Provider gives true in shallow comparison hence provider itself did not render.

6.1.(b)

Conclusion is if your component uses context value, then when the provider gets rendered and the context value is changed referentially, your component gets rendered.


7. Brainstorming query

When we render components at same level using map we need to pass key prop. Could you navigate through the above reconciliation process and think why using array indexes would be an anti-pattern?


要查看或添加评论,请登录

社区洞察

其他会员也浏览了