Achieving mastery of React's behaviour by reading Preact's source code ⚛️
In this post, I would like to share my experience on the journey of learning Preact's open source code to have a better understanding of React's behaviour or any Virtual DOM based libraries / frameworks in general. Stay tuned and read on guys 😎
As of writing, I'm working for Shopify and we are making commerce better for everyone in the world ❤️ If that sounds right to you, please don't hesitate to join us 😊
We decided to place our bet on React of Facebook / Meta team which is the most dominant library for building library in North America. Yet, as I have observed on the Internet and in my professional working experience - working with React at scale is very challenging. It's not because the library is not great, they have pioneered many great ideas ever since it's first open sourced back in 2013 with composition model, hooks, server components, et cetera. It's very difficult or even impossible for library at this age without having any drawback.
I personally found myself have to read both the old and new docs again and again to gain enough context and knowledge in order to share with my friends and colleagues at work. There are two prominent things I have found are React's useEffect
and composition model
design principle
First and foremost, React's useEffect is one of the most mis-used APIs of React on the planet 🌐!
Source - React doc
The doc's sidebar has 4 lengthy sections dedicated to effects to explain things from different angles. Even React's core team member is trying to advocate the web not to use it - it's mostly for reusable libraries / packages, not applications. Some mastermind believes useEffect is a bad abstraction as there are things it could not achieve and requires hard work from developers 😅
Secondly, modern UI architecture approaches are all about composition which is exactly how React / Vue / Svelte / Solid / Angular / ... have always been when it comes to their design principles. Back in the day, we have authored custom React's component by extending its core class and might think that's it's inheritance until hooks came out to go all-in for functional yet stateful component with hooks 😄 In the early days of React, they even have a dedicated section on composition vs inheritance to promote the practice among community and internal Facebook's usage 😜
Components written by different people should work well together
Even though I am capable of supporting my teammates understanding these concepts for better code quality. Yet, I have always felt something missing in my knowledge. I love OSS and always try to learn as much as possible in my free time to get out of my outdated comfort zone and level up myself. I have read source code of many great software ranging from small utility library like mitt, VueUse, Solid, Vue Reactivity System, et cetera. Yet, I am never motivated enough to read React's source code itself because I am not a fan of it ever since 2017's issue about its licensing manner. Lately, I decided to take another approach which both satisfy my inner peace and understanding React's behaviour better. I chose learning Preact 🤘
Now, I'm able to answer questions that I have always had about React which is not available in the doc:
- Why does parent's component code run first but its componentDidMount / componentDidUpdate / useEffect / useLayoutEffect run after its child's ones?
- Why does
useLayoutEffect
run synchronously and can cause performance hit? - What is the difference in implementation of
useEffect
anduseLayoutEffect
? - Why do the rules of hooks exist?
- Where are hooks stored internally?
- Why is
key
so important to diffing / reconciliation process? - How do componentDidCatch and getDerivedStateFromError work?
- How does React suspend and resume on lazy-loaded component?
- How can React safely ignore diffing on the same reference of elements?
- How does React know when to re-render all context-consumed components?
- How does React sync up Virtual DOM with real DOM?
- What is the final JavaScript code that got shipped to browser after transpiling JSX?
- How can React batch multiple synchronous state updates into a single render?
And of course, these concepts are now much easier for me to explain to anyone since I understand how things work under the hood now 😊
Source - Every React Concept Explained in 12 Minutes
The following is my high level understanding of how everything fit together internally:
-
We need a single root DOM element so that React can sync up their virtual DOM (VDOM) tree which is defined by developers
-
VDOM is defined by executing what so-called components. The result is a simple JavaScript object which store information about the VDOM such as:
2.1 type (div, p, h1, function component, class component, etc)
2.2 props is a custom object meant for communication among VDOM
2.3 key (which is used to facilitate diffing and whether or not to keep the old VDOM in the current render)
2.4 ref is meant for attaching DOM's reference or component to a variable
2.4 _children is meant for storing all children as an array
2.5 _parent is meant for storing parent's VDOM of current VDOM
2.6 _depth is for storing the current depth from the top - this is important for rerender because we want to let all synchronous operation happened first then process all components again in top-down manner. During diffing, child components will be marked
_dirty
as false to avoid unnecessary re-render unless they trigger update during rendering 😱2.7 _dom is for storing the current DOM element synced to the VDOM
2.8 _nextDom is for storing the next sibling DOM element which is useful for diffing process
2.9 _component is for storing component instance
2.10 constructor is for storing the constructor of component. This can either be string type (p or div tag) or function / class type
2.11 _original is for versioning VDOM at runtime which is again useful for reconciliation process
2.12 _index is for storing current index of VDOM so that it can be accessed later during diffing by parent VDOM
2.13 _flags is important for flagging component during diffing process according to pre-defined set of flags which then later used for cases such as inserting component to the DOM
-
After rendering all components, diffing and inserting / updating the DOM. It's time to flush all queued effects. Things like componentDidMount / componentDidUpdate / setState's callbacks. Because React run this process recursively from its top component to the bottom ones. Diff <-> Diff Children <-> Diff <-> Diff Children...<->Diff All callbacks will be collected in the bottom up depth-first manner which leads to lifecycle methods will be called from leaf components to the top ones 😎
-
Two APIs of React - componentDidCatch and getDerivedStateFromError are actually handled during _catchError event cycle of rendering process. It's bubbling up to the nearly component that has either of these two API defined 😁
-
Suspense is powered by throwing a promise and handle it properly for future use 😜
-
During diffing process, if React see the same _original properties on the VDOMs, it will reuse the results from previous VDOM and skip diffing early on that node (of course all of its children as well 🤯). That explains why we are able to achieve this simple trick to optimize React re-renders 🚀
-
Provider in React is typically implemented by pub-sub pattern where all consumers register to the publisher (Provider). Whenever context value changes, publisher will notify all subscribed components to be enqueued to re-render accordingly 🤸
Before we are going to the fourth step. It's worth highlighting upfront that Preact does expose internal options so that hooks can 🪝 into lifecycle events during rendering process 🤘
-
This getHookState function is probably the most important function of how things work in hooks API which explains a whole lot about the rules of hooks. Initially, the first hook's index is set to 0 which is then incremented every time we use hook on each render. That's why we can't conditionally apply hooks 🎉
-
The reason why
useEffect
is recommended instead ofuseLayOutEffect
is because the first one defines a list of pending effects which will be run after browser's next paint. The latter one will queue effects to be run after render synchronously like step 3 above 😄 -
Many hooks are built upon others like useState built upon
useReducer
, useCallback and useRef built uponuseMemo
, useImperativeHandler built on top ofuseLayoutEffect
There you have it guys, I hope you find this post useful and enjoy the benefits of completely understand everything inside out instead of relying heavily on what has been written in the doc. The doc is good if it's well-maintained by human which requires huge amount of effort. I still believe the best documentation in the world is the source code itself. It's definitely not the easiest way yet the best way to gain mastery of something you use. Together, we make the world a better place through quality software 💞
❤️❤️❤️ Be well, Be happy ❤️❤️❤️