How Vite helped me on my React source code exploration adventure.

How Vite helped me on my React source code exploration adventure.

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

  1. 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>
  1. Self bundles creation from React source code

    According to React contribution docs you can build the source code with yarn build and open the fixtures/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 using vite-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 the index.html files

  • Update 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 uses flow.js for typing, necessitating transpilation before it can be sent to the browser. Fortunately, there's good news: there exists a Vite 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 in devDependencies and run yarn

  • Load the flow plugin
    Create vite.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 or rollup. Since I'm not utilizing webpack or rollup, I need to manually provide these values. I leverage Vite's global variable feature for this purpose. Subsequently, I include the necessary line in the Vite 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 in packages/react/index.js.
Notice that we are in the react package and trying to import something from the shared 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.