Diving into the depths of React.js source code or embarking on a journey of reverse engineering can be an exhilarating endeavor. To complete your journey, you'll need a small React application to act as your compass. So, how can you preview this small app ? The essential step for most individuals is to create an HTML file and utilize a Content Delivery Network (CDN) to import React.js scripts. This approach typically fulfills all necessary requirements. However, for those interested in reverse engineering or seeking a more comprehensive exploration experience, there exists another method. In this post, I'll show you how I created this application using React's direct source code with vite.js.
Let's delve into how it's accomplished with HTML and React.js builds
- Using React CDN
<html>
<body>
<script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
<div id="container"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Hello World!</h1>,
document.getElementById('container')
);
</script>
</body>
</html>
Self bundles creation from React source code
According to React contribution docs you can build the source code with
yarn build
and open thefixtures/packaging/babel-standalone/dev.html
to preview the result of the build. The contents of this file are the same as above, except that it loads the bundles generated by the yarn build command.
Is there any drawback to using bundles in your exploration journey ?
While bundles serve a purpose, I believe there's another approach that offers a superior exploration experience. Bundles do have their limitations:
You can't easily get to grips with module organization while debugging. There's no
.map
files.When you make some changes in the code you need to re-build each time. There's no existing auto-reload features.
I tried to change the build configs to create .map
files, but I get an error.
How Vite
helped me ?
During my exploration of the React source code, I promptly encountered the constraints I previously mentioned. I pondered on a method to bring the source code directly to the browser without resorting to bundles. Knowing that Vite
operates in this manner, I decided to leverage it.
I learned that the React codebase utilizes the yarn
workspaces feature, which implies that I can conveniently create a new package (workspace) within these workspaces to share React's source code. This is precisely what I proceeded to do. However, I encountered additional challenges along the way. In the subsequent lines, I will elucidate on my implementation.
1. I create new workspace with me example application code using Vite
.
- In the
packages
folder in React source code, I create new project usingvite-create
yarn create vite preview-react --template vanilla
# You might ask why use the vanilla template and not this React one?
# Because the vanilla template is easier to clean.
# The command will create `preview-react` folder
Delete all the generated files without
package.json
and theindex.html
filesUpdate the
index.html
file as follows
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React preview</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./App.jsx"></script>
</body>
</html>
- Create
App.jsx
file with the content below
import * as React from 'react';
import { useState } from 'react';
import { createRoot } from 'react-dom/client';
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</>
);
}
const root = createRoot(document.getElementById('app'));
root.render(<Counter />);
2. Add React
dependencies
I require two dependencies: react and react-dom. To obtain their version numbers, I navigate to the package.json file within each respective package (located in the packages folder of the React code base). Upon noting down the version numbers, I update the package.json file of the preview-react workspace with the identified versions.
{
...
"dependencies": {
"react": "put-identified-react-version-here",
"react-dom": "put-identified-react-dom-version-here"
}
}
3. Create custom vite
config
We need to add some custom configs to make vite
work properly with react code base.
- Transpile flow files
React code base usesflow.js
for typing, necessitating transpilation before it can be sent to the browser. Fortunately, there's good news: there exists aVite
plugin tailored for this purpose.
yarn add -D @bunchtogether/vite-plugin-flow
If the installation fails with an error like
An unexpected error occurred: "expected workspace package to exist for "..."".
, try to manually add@bunchtogether/vite-plugin-flow
indevDependencies
and runyarn
- Load the flow plugin
Createvite.config.js
file with the content below
import { esbuildFlowPlugin, flowPlugin } from '@bunchtogether/vite-plugin-flow';
import { defineConfig } from 'vite';
const flowPluginOptions = {
include: /\.(flow|js?)$/,
exclude: /node_modules/,
flow: {
// The `all` flag is set to `true` to force transpiling of all files.
// The plugin expects files typed with flow to include the `@flow` annotation.
/// But not all react modules include this annotation, even if they are
// flow-typed.
all: true,
pretty: false,
ignoreUninitializedFields: false,
},
};
export default defineConfig({
plugins: [flowPlugin(flowPluginOptions)],
optimizeDeps: {
esbuildOptions: {
plugins: [
esbuildFlowPlugin(
flowPluginOptions.include,
undefined,
flowPluginOptions.flow,
),
],
},
},
});
Create some React global values
The React source code depends on certain global variables that are typically defined or replaced at build time by tools like
webpack
orrollup
. Since I'm not utilizingwebpack
orrollup
, I need to manually provide these values. I leverageVite's
global variable feature for this purpose. Subsequently, I include the necessary line in theVite
configuration.
...
export default defineConfig({
plugins: ...,
optimizeDeps: ...,
define: {
__DEV__: false,
__EXPERIMENTAL__: true,
__EXTENSION__: true,
__PROFILE__: false,
__TEST__: false,
},
});
4. Resolve some importation in React code base
This section of the article might pose a challenge for understanding. Allow me to clarify the necessity of this resolution.
Take a look at the line 22 of this file packages/react/src/ReactServerContext.js
of React code base, you will see this importation.
// packages/react/src/ReactServerContext.js
import ReactSharedInternals from 'shared/ReactSharedInternals';
This module is imported in
packages/react/src/React.js
this one imported inpackages/react/index.js
.
Notice that we are in thereact
package and trying to import something from theshared
package.
Now let's take a look at the shared/ReactSharedInternals
file at its line 10, the content is below
import * as React from 'react';
const ReactSharedInternals =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
This module depends on
react
package.
There is a circle dependency. Take a look at the graph below.
Let me show you another stuff. Go to the packages/react-reconciler/src/ReactFiberHydrationContext.old.js
file. At its line 42 you will see some import from packages/react-reconciler/src//ReactFiberHostConfig
.
// We expect that our Rollup, Jest, and Flow configurations
// always shim this module with the corresponding host config
// (either provided by a renderer, or a generic shim for npm).
//
// We should never resolve to this file, but it exists to make
// sure that if we *do* accidentally break the configuration,
// the failure isn't silent.
throw new Error('This module must be shimmed by a specific renderer.');
The module triggers an error unconditionally. and notably lacks any export statement. However, it includes a clear comment.
What you need to understand is that during build certain imports are resolved in a specific way. For example, the circular dependency that I'm talking about a little earlier will be fixed, by importing the react/src/ReactSharedInternals
module instead of react
in the shared/ReactSharedInternals.js
file. And the import of react-reconciler/src//ReactFiberHostConfig
in module react-reconciler/src/ReactFiberHydrationContext.old.js
will be replaced by react-reconciler/src/forks/ReactFiberHostConfig.dom.js
in as part of a build for the web.
There are many similar cases in the code base. The resolution of the two that I talked about allows React to work in the browser, we are not going to worry about the rest. If you're interested in seeing how the resolution is done, then take a look at this file. The easiest way to make the vite
project work quickly
with these new elements is to modify the corresponding files with the correct imports. This involves modifying the React source code. Alternatively, the resolution can be handled in the Vite config by creating a plugin (which I've done, although I won't include those details in this article; you can check them here.
Let's do it quickly by modifying the codebase.
- Change import in
packages/shared/ReactSharedInternals.js
file. Replace its content by the follow.
import ReactSharedInternals from 'react/src/ReactSharedInternals';
export default ReactSharedInternals;
- Replace the content of
packages/react-reconciler/src/ReactFiberHostConfig.js
by the content below.
export * from 'react-reconciler/src/forks/ReactFiberHostConfig.dom.js';
Now there are all necessary to the preview app to works.
This makes exploration much smoother.
Conclusion
Exploring React's source code in order to gain an in-depth understanding of its operation and architecture is an exhilarating undertaking. We need a small React application as a guide. Generally speaking, this is an HTML page into which we import React's scripts (bundles). This technique, while satisfactory, has some limitations: slow build times and lack of source maps.
In this article, I presented an alternative method using Vite
, bypassing the need for bundling and bringing React source code directly to the browser. This involved creating a new workspace within React's yarn workspaces feature and configuring Vite
to handle Flow.js files and resolve specific imports.
I create a public repo that includes the example application with
Vite
, find it here.