Level 8: Wrapping Everything Up
Introduction
Now that we have looked at all the functionality SuiWeb provides, we are going to look at all the calls that happen when we render an app with SuiWeb. We will no longer look into the implementations of the functions, but just at the order, in which they are called.
Goal
The demo we set up for this level consists of three components, App
, DateComponent
and Counter
. The component App
returns a h1
title, a DateCompoennt
and a Counter
, which are all wrapped in a div
. The DateCompoent
returns a string which contains a timestamp of the date when it was rendered. In the Counter
component, we return another DateComponent
and a button
showing the amount of times it has been clicked. The Counter
component is also wrapped in a div
, which has a backgroundColor
of Aquamarine
. This helps us to better see what is contained inside this component.
If you click the button, you can see that its count is incremented. As this changes the state of the Counter
component, it will be re-rendered and thus the DateComponent
inside Counter
will be re-executed, resulting in its timestamp being updated. If you look at the console, you can see that an effect is called every time the button is clicked, as the count
constant changes.
As the other DateComponent
is added inside the App
component, it will not be re-rendered if the button is clicked, as the state of the button is not defined in App
. This illustrates that only the components (and their subcomponents) are re-rendered that contain a state that has changed.
Below, you can see the source code of our index.js
. Our index.html
is again the same as in all other levels that used SJDON.
index.js
import { createElement } from '../../lib/js/fiber.js'
import { useEffect, useState } from '../../lib/js/hooks.js'
import { render } from '../../lib/js/render.js'
import { parseSjdon } from '../../lib/js/sjdon.js'
const App = () => {
return [
'div',
['h1', 'Wrap Up'],
[DateComponent],
[Counter]
]
}
const DateComponent = () => {
return [
'p',
'If value changed, the component was rerendered: ', ['strong', Date.now()],
]
}
const Counter = () => {
const [count, setCount] = useState(0)
useEffect(
() => console.log(`The value of count has changed: ${count}`),
[count]
)
return [
'div',
{
style: {
backgroundColor: 'Aquamarine',
padding: '1rem'
}
},
[
'button',
{ onclick: () => setCount(count + 1) },
`Clicked ${count} times`
],
[DateComponent]
]
}
const rootFiber = parseSjdon([App], createElement)
render(rootFiber, document.getElementById('app'))
First Rendering of our App
We will create a list representing the order of functions that are called for all the calls that happen when rendering App
the first time.
Parsing SJDON
First, let's see the calls needed to parse our SJDON array into Fiber
s.
const rootFiber = parseSjdon([App], createElement)
to create fibers from SJDON- If
typeof type == 'string'
, we callcreateElement(type, props, ...parsedChildren
- Else we call
createElement(fiberFunction, props)
- We do the same for all children of the fiber
- If
Rendering the Root Fiber
We call render(rootFiber, document.getElementById('app'))
using our now parsed root fiber and a reference to the container in which we want to render the root fiber. We will make sure in this function that all other elements contained inside our container will be removed. Then, we start with the actual rendering.
renderFiber(fiber, container)
unwrapFunctionalFiber(fiber, container)
if the fiber which we want to render is aFunctionalFiber
prepareToUseHooks(fiber.memorizedStates, () => rerenderFunctionalFiber(fiber, container)
to setup hooks, register the re-render function for the componentfiber.fiberFunction(fiber.functionProps)
to execute functional fibers- Calls to
useState
anduseEffect
of the that fiber are made - Actions of
useEffect
calls are scheduled for next event cycle usingsetTimeout(action)
- Calls to
Object.assign(fiber, unwrappedFiber)
to merge the original functional fiber with its unwrapped version, while keeping the same object
replaceFiberInDom(fiber, container)
as there is no previous version of our fiber, thusareSameType
will always be false for the initial renderingcreateDomNode(fiber)
to create an HTML element from our fibercontainer.insertBefore(newDomNode, nextSibling?.domNode ?? null)
to add our HTML element to the DOM
expandChildFibers(fiber)
to go through all the children of the fiber we passrenderFiber(currentChild, container, previousChild, nextChildSibling)
for each child, repeat all the steps in this list
- Actions of
useEffect
calls are executed when rendering has finished, as the next event cycle starts then
Re-Rendering Due to a Change in a State
Imagine what happens now when we click the button inside our Counter
component, which calls setCount(count + 1)
, that corresponds to the setState
function of our counter
state.
- Set the new value passed to
setState
insidecapturedStates
capturedRerender()
to execute the re-render function that was captured inside thesetState
closure before and registered usingprepareToUseHooks
rerenderFunctionalFiber(fiber, container)
is called, as this was stored inside thecapturedRerender
closurerenderFiber(fiber, container, previousVersion)
to re-render the fiberunwrapFunctionalFiber(fiber, container, previousVersion)
- Copy over the
memorizedStates
array ofpreviousVersion
to the new fiber - Re-execute the fiber's
fiberFunction
- Calls to
useState
anduseEffect
of the that fiber are made (state now contains updated values) - Actions of
useEffect
calls are scheduled for next event cycle usingsetTimeout(action)
, if one of the effects dependencies has changed
- Calls to
Object.assign(fiber, unwrappedFiber)
to merge the original functional fiber with its unwrapped version, while keeping the same object
- Copy over the
const areSameType = fiber && previousVersion && fiber.type === previousVersion.type
to check whether the new fiber is of the same type as its previous version was- If
areSameType
,updateFiberInDom(fiber, container, previousVersion, nextSibling)
updateDomNode(domNode, previousVersion?.props, fiber.props)
to update the HTML element that already exists on the DOM with its new props- If the order of the element has changed, re-insert the element in the DOM (which automatically removes the element at its other position in the DOM) by calling
container.insertBefore(domNode, nextSibling?.domNode ?? null)
- Else
replaceFiberInDom(fiber, container, previousVersion, nextSibling)
createDomNode(fiber)
to create an HTML element from our fibercontainer.insertBefore(newDomNode, nextSibling?.domNode ?? null)
to add our HTML element to the DOM
- If
expandChildFibers(fiber)
to go through all the children of the fiber we pass
renderFiber(currentChild, container, previousChild, nextChildSibling)
for each child, start again fromrenderFiber(fiber, container, previousVersion)
- Actions of
useEffect
calls are executed when rendering has finished, as the next event cycle starts then
Review
With that, the final level of this tutorial is coming to an end. The lists above, showing the order in which calls are made as part of rendering our fibers, might support you to get an idea how a whole rendering cycle works. It might be helpful to also look at the source code of SuiWeb, while going through the lists of calls that are made. We have realized ourselves that it's sometimes hard to understand which functions are called as part of a rendering cycle, when we analyzed the source code of React and Build your own React, so we hope that this list can give you at least some help to better understand this whole process.
We hope that this tutorial helped you to understand how you can use SuiWeb, but also how its functionality is achieved internally. If you like to build applications in that way, you might want to have a look at React, the framework whose concepts we tried to explain with SuiWeb. React comes with a lot of additional functionality and performance optimization, which make it much better suited for production ready applications.
Thanks for working through our tutorial 🙂