Meact.js - My [Custom Implementation of] React.js
I developed Meact.js as a highly stimulating and educational project, where I built a core "Render Tree" and a DIFF reconciliation algorithm inspired by React with APIs for most commonly used hooks. This includes a client-side renderer similar to "react-dom."
Additionally, I have created a ["Multi-Page Application" (MPA) framework]((./meact-framework/README.md) atop the "meact" and "meact-dom" libraries. This framework uses server-side routing, with a page-based routing pattern inspired by Next.js and a server-side loader/action pattern borrowed from Remix.js. The setup leverages Esbuild for preparing tree-shaken and code-split JS/CSS bundles at build time, while Express.js dynamically serves these pages by injecting the appropriate bundles into "index.html" and running loaders through middleware.
See the HackerNews Clone app built using this framework to test and prove its production readiness
-
Functional Components (i.e. reusable UI elements) built with "Template Literals" and global State & Event Handler Managers
-
React-like (
createElement
andrender
) API -
Declarative UI Programming using library's functions
-
Render Tree (of
MeactElement
objects as nodes)- Render Tree is plotted in browser as well for debugging
-
Event Handlers
-
props
-
Children Elements & Components (and
children
as render props) -
useState
(I neededclosure
!) -
DIFF Reconciliation during Re-rendering
-
useEffect
-
useRef
-
Custom Hooks
-
useMemo
-
useCallback
(redundant afteruseMemo
)
-
-
memo
-
createContext
anduseContext
(! Scope for improvement) -
JSX Syntax
-
Esbuild Bundler (to manually compile JSX into
createElement
calls) -
/jsx-runtime
Entrypoint API -
MDX Support (using MDXjs compiler)
-
Virtual DOM Nodes (i.e.
DocumentFragment
nodes) -
Multi-Page Application (i.e. Server-side Routing)
-
Page-based Router
-
Client-Side Rendering (CSR)
-
Tree Shaking (by routes)
-
Code splitting (chunks shared between routes)
-
Meta Tags (Server-side Hydration)
-
Server-side Loader and
useLoaderData
Hook Pattern (like Remix.js) which prepares data needed by page (and its components) at request time and sends it alongside HTML content thus saving a round-trip -
Server-side Action and
useActionData
Hook Pattern (like Remix.js) using customFormAction
component anduseMutation
hook see "Submit Page" implementation -
Framework's Abstraction
-
A component must be defined as a named function only, and not as an anonymous function.
- Why is this okay?: Because anonymous functions as component definitions don't bring any benefits in ergonomics, while an anonymous component function is bound to be recreated on every render (due to a new node ID with the render tree each time) and it could be harder to debug in stack traces or logs as well.
-
app/index.js
should default export component that should be rendered for a URL/path, and each page component (insideapp/pages/*.js
) should also default export their corresponding function definitions.
-
DIFF Reconciliation: Reconciliation is the algorithm behind what is popularly understood as the "virtual DOM." When the application is rendered, a tree of nodes (i.e. the "render tree") that describes the app is generated and saved in memory. The renderer translates the render tree into a set of DOM operations. It's designed such that reconciliation and rendering are separate phases where the reconciler does the work of computing which parts of the tree have changed and the renderer then uses this information about the minimum set of changes required to update the actual DOM so as to update the rendered app. (This is why "virtual DOM" is a bit of a misnomer.)
- Fancy side note: We could maybe say that the render tree is a more specific implementation that embodies the concept of the virtual DOM. Like, Inversion of Control (IoC) is a design principle (concept) where the control flow of a program is inverted from our custom logic to the framework or runtime and the Spring frmaework (an IoC container) implements that as dependency injection using reflection at runtime.
-
useMemo
caches results of expensive calculations or references to arrays/objects/functions so that these result values or references are not recreated across re-renders, are then same values across renders when passed viaprops
and are thus not queued as "updates" in the browser DOM by the UI library during a re-render.-
React's doc says: The only benefit to
useCallback
is that it lets you avoid writing an extra nested function inside. It doesn’t do anything else.function useCallback(fn, dependencies) { return useMemo(() => fn, dependencies); }
-
-
memo
is a different optimization feature which is used to force stop the evaluation and thus updation of a child subtree (in the "Render Tree") itself when its parent component re-renders given child'sprops
are same across renders.
-
In-browser debugging is very robust, got to play with "Sources" tab a lot while debugging recursive library functions & APIs.
-
React uses the order of hooks inside a component's function definition to manage state and side effects.
-
I have actually understood the importance of immutability in object references and its impact on work done during re-rendering given how React.js uses shallow comparison to strike a balance between performance and correctness.
- If a property itself is an object (or array), a shallow comparison will only check if the references (or addresses) of these objects are the same, without recursively comparing nested objects or arrays. For insatnce, defining objects in inline
style
attribute will create a new object reference on every render and thus resulting in re-rendering of that element in the browser DOM - so, either define them outside the component or useuseMemo
.
- If a property itself is an object (or array), a shallow comparison will only check if the references (or addresses) of these objects are the same, without recursively comparing nested objects or arrays. For insatnce, defining objects in inline
-
How different frameworks handle the difference between DOM properties and attributes
-
A
JSX Runtime
is a set of functions that a compiler (like Babel) uses to transform JSX syntax into JavaScript function calls. In the React ecosystem, these are provided by the "@jsx-runtime/react" package. Instead of just a singlecreateElement
function, modern JSX runtimes usually define:Fragment
: A component used to group multiple elements without adding extra nodes to the DOM.jsx
: For handling JSX elements.jsxs
: For handling JSX elements with multiple children (used for optimization).jsxDEV
: A version of jsx with additional development features (like better error messages).
-
DocumentFragment
: To mimic React.js'sFragment
or to create a DOM node respective to a functional component node from the render tree, we can use aDocumentFragment
object.- A
DocumentFragment
is a lightweight, minimalistic document object that can hold and manipulate a group of nodes. When we append aDocumentFragment
to the DOM, its children are appended, but the fragment itself is not (similar to how a ReactFragment
works). That is, just like a ReactFragment
, aDocumentFragment
doesn’t create an extra node in the DOM tree, and only its child elements are added. But,Document
andDocumentFragment
nodes can never have a parent, soparentNode
will always returnnull
.
- A
-
React Compiler is a new experimental compiler which requires React 19 RC. It automatically memoizes code using React's rules. Also, React Compiler's eslint plugin can be used independently of the compiler to display any violations of the rules of React in your editor.
-
To write JSX synatx which is not natively understood by browsers, we need a transpiler that transforms JSX into calls to appropriate library function calls.
-
To write modern JavaScript which is not natively understood by older browsers needs a transformer.
- E.g. use of different module systems (e.g., CommonJS, ES modules)
-
To package (or bundle) multiple JavaScript files by resolving their dependencies into a single file (or a few files) that can be efficiently loaded by a web browser.
- To bundle a file means to inline any imported dependencies into the file itself. This process is recursive so dependencies of dependencies (and so on) will also be inlined. By default esbuild will not bundle the input files.
-
I keep forgetting to re-generate the bundle after editing code, besides it being a unproductive flow-breaker. So, "live reload" would be much appreciated!
-
When you import a script via a
<script>
tag, the code is expected to be directly executable in the browser without any additional environment.-
require()
is a Node.js module system function, and it's not supported in browsers. -
ES Modules (ESM) are a standardized module system in JavaScript. They use
import
andexport
statements and are supported by modern browsers. However, for the browser to recognize and correctly parse a JavaScript file that uses ES Modules, you need to indicate this with thetype="module"
attribute in the<script>
tag. But,type="module"
causes following issue which can be solved using a server.Access to script at '.../components.js' from origin "null" has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome, https, chrome-untrusted.
-
By using format: "iife", you ensure that the output is wrapped in an Immediately Invoked Function Expression, which allows it to run directly in the browser.
-
meact.js does synchronous state updates instead of batching them and applying them in an asynchronous manner like React.js, and its performance implication ("CPU fan noise") can be observed by running ./lab/15-react-sim-sync-setstate-performance/index.html
where the UI implements a React Component which renders a colored circle following the mouse position, and another colored circle following first circle's position delayed by 100ms and so on (for total 5-6 circles).
Why is setState
in react.js Async instead of Sync?
- In a UI, it's not necessary for every update to be applied immediately; in fact, doing so can be wasteful, causing frames to drop and degrading the user experience.
- Different types of updates have different priorities — an animation update needs to complete more quickly than, say, an update from a data store.
- A push-based approach requires the app (you, the programmer) to decide how to schedule work. A pull-based approach allows the framework (React) to be smart and make those decisions for you. React doesn't currently take advantage of scheduling in a significant way; an update results in the entire subtree being re-rendered immediately. Overhauling React's core algorithm to take advantage of scheduling is the driving idea behind Fiber.
- A simple example to demonstrate this, is that if you call
setState
as a reaction to a user action, then the state will probably be updated immediately (although, again, you can't count on it), so the user won't feel any delay, but if you callsetState
in reaction to an ajax call response or some other event that isn't triggered by the user, then the state might be updated with a slight delay, since the user won't really feel this delay, and it will improve performance by waiting to batch multiple state updates together and rerender the DOM fewer times.
Beyond many for-scale things, meact.js doesn't implement these necessary ones (versus react.js)
- Great documentation, extensive testing and OSS community
- Dev tools and Hot Reload in dev environment
- Scoped CSS; CSS-in-JS
- Synthetic events and Cross-browser API for setting DOM node attributes and properties
- Ecosystem of libraries and frameworks (e.g. Astro, Next.js, client or async state management, UI components, caching, etc)
- Metadata, SEO tuning, robots.txt, sitemap
- Server side rendering
- Concurrent rendering
- Performance optimizations:
- Scheduling of state updates
- Memory management (e.g. garbage collection)
- Compiler like Svelte/Solid (under development)