Why did React render your component?
Tl;dr
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),
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):-
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)
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.1 Render phase
Reconciliation process:-
4.2 Commit phase
4.3 Example code
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:-
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.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.
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.
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?