aka: how to effectively update the UI when state changes and render function returns different tree of elements
Checking the full tree changes with super-duper optimized algorythm would have complexity of O(n^3)
(!!). React instead uses heuristic alghorithm (O(n)
):
- Two elements of different types will produce different trees.
- The developer can hint at which child elements may be stable across different renders with a key prop.
Those assumptions are valid for most cases - so it's good enoguh (better than making billions of comparisons)etc.
When type of element changes from for example a
=> img
or Article
=> Comment
everything below will be unmounted (destroy DOM nodes, unmount components) and then new elements will be inserted (componentWillMount, componentDidMount). Any state is lost. When parent changes ALL children are destroyed even if they are the same component:
<div>
<Counter />
</div>
<span>
<Counter />
</span>
This will unmount and remount Counter
.
React looks at what attributes changed and applies them to the underlaying DOM node (ex. changes in className
or style
)
Instance stays the same, state stays the same, props are updated, render()
is run. The algorythm recurses inside what the render
returned
When added to the end - no problemo When added at the start - re-render everything (oh, no)
To fix this - use key
attribute
Protip: if you don't have anything to use as key, you can add new ID property to the model or hash some parts of content to generate a key. It has to be unique with the siblings (no globally).
Using index when iterating works as long as you don't re-order the elements (then re-render everything on every change). It has other problems so don't do it at all cost
This reconciliation is just an implementation detail (it can change over time to optimize new use-cases).
Also if the assumptions are not met - the app performance will suffer:
- The algorithm will not try to match subtrees of different component types. If you see yourself alternating between two component types with very similar output, you may want to make it the same type. In practice, we haven’t found this to be an issue.
- Keys should be stable, predictable, and unique. Unstable keys (like those produced by Math.random()) will cause many component instances and DOM nodes to be unnecessarily recreated, which can cause performance degradation and lost state in child components.
It's the newest reconciliation algorithm from React 16.
It was created so that React could be used not only on the web but also for: hardware (VR), mobile apps (React Native). This created a difference between reconciler and the renderer.
Renderer can be provided by community to target other platforms outside the DOM. There is only one reconciliation alg.
Our code is like PM in a project - giving stuff to do And it has only one team member - main thread (kinda like Full Stack dev) - it does all the things (JS, CSS animation, paint. If it's stuck on one thing then, it can't do anything else (that's where the dropped frames come from)
React comes to project as Tech Lead and it knows how to optimize the work done by the main thread. It makes it more efficient by minimizing DOM changes.
React has skills that help them do that: split chunks of work, has a watch to look how much time has passed
It does work layer, by layer. Create element => Create instance => Create DOM node (or update).
It does this work until it goes to the bottom of the tree.
There are high-priority updates (user typing) and low-priority updates (updates from fetch)
It makes it possible for main thread to compute a bunch of fork on the tree, come back up to see if it has other work to do:
The way it keeps track of where it is in the tree is by fiber nodes
Each node has stateNode
. #todo
First fiber is HostRoot
- it corresponds to the container that the React tree was injected into (ReactDOM.render()
).
It's first child is the root App component that we've also passed to the ReactDOM.render
.
Both nodes have a relationship by using child
and return
properties:
When there are multiple elements inside an element fiber nodes use sibling
to represent that relationship. Eeach sibling point to the next one
Current fiber tree is constructed during the initial render
There's also a workInProgress tree. Why we need it? We don't want to make changes in the DOM while we're computing changes in the tree.
The previous reconciler while it was working on the changes it also made the changes to the DOM immediately:
This works if working synchronously. But when we stop making those changes browser will make a paint (it'll flush the changes onto the screen) so our UI will look inconsistent.
To combat that Fiber introcuted phases:
- render / reconciliation -> figure out what needs to change (interruptible)
- commit -> actually make changes to the DOM (not interruptible)
When called setState
this action is added to update queue. Then the work is scheduled (to make this a reality).
To request action that doesn't need to happen immediately React uses requestIdleCallback
React: Hey, Main Thread! Let me know when you have spare time to do this work
After that main thread will respond and tell React how much time it has to spare (13-15 ms)
It allows react to do some computation, interrupt and let work to happen in the main thread.
To do this:
- Next unit of work
- Time remaining within the main thread (got by
requestIdleCallback
)
React will start copying stuff from current
tree to workInProgress
as it is happening.
The update queue will be copied as well. After that react checks if it still has time in the main thread. If it does it looks at the update queue, calls the updater function and updates the state.
This fiber in workInProgress will have a tag attached - it means that there's need to be change to be made in the DOM tree.
When state changed react will call render to get list of children elements.
It will then compare if the items from the current tree match the items from the list returned by the render function.
- If they match they'll be just cloned from the current tree
After that react goes into another unit of work - which means checking if it still has time on the main thread (this goes on, and on recuricively)
If the element doesn't have any children and there isn't anything that needs a change in the DOM - React will just complete the unit of work.
* for functional components React.memo()
can be used instead
If shouldComponentUpdate
returns false then the unit of work is completed.
Fiber node keeps track of all the changes underneath it (in it's children or siblings).
When children needs to have a change it will push all of it's changes to the parent's effect list
The algorythm
- Unit of work started.
- Clone thing from working tree
- Has update queue? Do the updates - add them to the effects list
- Has children? Render them, compare with working tree
- If the same = clone from working tree
- If not the same = add to workInProgress and mark it to render
- Add own effect list to the parent
- Return next unit of work (child or sibling)
- Check if we still have the time on the main thread
When the workInProgress tree is finished React marks it as pendingCommit. This is the end of the first Page.
React will check it's deadline - if it has time it will do it now, if not then it'll use requestIdleCallback
again. After it receives control it will make the commit.
React will go throguh the effect list starting with the first fiber (deepest in the tree).
After the changes will be made the now current tree would be outdated - to reflect this change React will switch pointers between current and workInProgress (efectively swapping them).
This is called double buffering - it saves time on memory allocation and garbage collection.
React will go through the effect list once again and:
- call the lifecycle hooks
- re-attach refs (they would have been de-attached during commit phase)
- handle error boundaries
After everything - we've finished the commit
Each update has priority assigned:
- Synchronous (works the same as stack reconciler)
- Task - before next tick of event loop
- Animation - before next frame
- High - pretty soon
- Low - ex. data fetching
- Offscreen - hidden, prepare for display scroll
This means that if high priority work would appear then it'll jump up ahead of low priority update even if it has started! - that's why we need Fiber
When that happens it throws out any work that has been done to handle the priority update. It'll start again creating workInProgress tree from HostRoot
.
The order they might fire is not the one we might expect. Some of them are called in Phase 1 - render / reconcilation, some of them in Phase 2 - commit:
The order could be:
- (update 1) componentWillUpdate
- (update 2) componentWillUpdate, componentDidUpdate
- (update 3) componentWillUpdate, componentDidUpdate
This happens when the high prio updates keep jumping in fornt of lower priority updates the queue so that the low prio update never happens.
How React handles that? Re-uses work when it can. When low prio work was done and high prio didn't touch that part of the tree then React can re-use that work which was made for low priority update.
(there might be more solutions to this)
cooperative scheduling - it's basically what the Fiber is doing - breaking work into smaller units of work, that can be paused so the main thread is free to do other important stuff
React Fiber Architecture What The Heck Is the Event Loop Anyway? To Understand React Fiber, You Need to Know About Threads
Activities that are performed during the reconciliation are called "work" in the Fiber architecture. Other "work" that wasn't described in the #reconcilation:
- calling lifecycle methods
- updating refs
The work depends on type of React element (component / functional / portal / host components (dom nodes)). The element type is defined by first parameter passed to the createElement
function.
The call to the createElement
returns internal data structure used by React:
[
{
$$typeof: Symbol(react.element),
type: 'button',
key: "1",
props: {
children: 'Update counter',
onClick: () => { ... }
}
},
{
$$typeof: Symbol(react.element),
type: 'span',
key: "2",
props: {
children: 0
}
},
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ClickCounter
}
]
$$typeof
property is added to objects to uniquely identify them as React elements
During reconciliation this data is merged into tree of fiber nodes. Every React element has a tree of fiber nodes. Unlike the elements (returned by render function) the fiber nodes are not re-created on every render. The element is turned into corresponding fiber node type (dom element / class component / etc.)
You can think of a fiber as a data structure that represents some work to do or, in other words, a unit of work. Fiber’s architecture also provides a convenient way to track, schedule, pause and abort the work.
The list of activities performed for existing fiber nodes is visible in the ChildReconciler function
Fiber nodes are connected through #linked_list by properties:
- child
- sibling
- return
- these:
- alternate (points to
current
tree orworkInProgress
tree counter part - see below) - effectTag (see #side_effects)
After render we have current tree. When working on updates to be flushed on screen we have workInProgress tree.
Why 2 trees? Because we don't want to show changes until we work them out.
React goes throguh the current tree, it creates an alternate node in workInProgress tree. The nodes are created from the data returned by render function. Once the tree is rendered to the screen it becomes the current tree.
#side_effects
UI = fn(state)
Every other activity (mutating DOM, calling lifecycle methods, etc.) are considered as side-effects. They can affect other components and can't be done during rendering.
Applying side-effects is work, so they are represented by fiber nodes.
- For DOM elements => add, update or remove element
- For class components => update refs, call lifecycle methods
React builds linear list of fibers with effects for quick iteration by adding nextEffect
property. This way when processing updaes it can go fast throguh linear list instead of a tree:
#todo