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 Fibers.
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 aFunctionalFiberprepareToUseHooks(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
useStateanduseEffectof the that fiber are made - Actions of
useEffectcalls 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, thusareSameTypewill 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
useEffectcalls 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
setStateinsidecapturedStates capturedRerender()to execute the re-render function that was captured inside thesetStateclosure before and registered usingprepareToUseHooksrerenderFunctionalFiber(fiber, container)is called, as this was stored inside thecapturedRerenderclosurerenderFiber(fiber, container, previousVersion)to re-render the fiberunwrapFunctionalFiber(fiber, container, previousVersion)- Copy over the
memorizedStatesarray ofpreviousVersionto the new fiber - Re-execute the fiber's
fiberFunction- Calls to
useStateanduseEffectof the that fiber are made (state now contains updated values) - Actions of
useEffectcalls 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.typeto 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
useEffectcalls 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 🙂