🇰🇷 Korean README
This is an app service that visualizes the component hierarchy of a React project in a tree structure.
- Preview
- Motivation
- Challenges
- Tech stacks
- Features
- Timeline
đź”˝ Rendering the tree structure after folder selection
đź”˝ Zoom in/out of the tree structure, adjust the slider bar, mouse events on nodes (hovering, clicking)
When I first started learning React or when I read someone else's React project code for the first time, it took time to understand the overall component structure.
I thought, "Wouldn't it be helpful for developing with React if we could visualize and display the structure of the rendered components?" This thought led to the start of this project.
// API to retrieve GitHub repository information
async function getGit(owner, repo, path) {
const dataResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`);
const data = await dataResponse.json();
const blobsResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/blobs/${data.sha}`);
const blobs = await blobsResponse.json();
console.log(atob(blobs.content));
return blobs;
}
await getGit("pmjuu", "my-workout-manager", "src/App.js");
đź”˝ Console output: It was possible to retrieve the file code from an actual repository as a string.
=> We concluded that it would be too difficult to implement parsing the string into JavaScript syntax without using an external library due to the many edge cases, making it hard to achieve within the time limit.
import React from "react";
import ReactDOMServer from "react-dom/server";
// Component to check
const MyComponent = () => {
return (
<div>
<h1>Hello, world!</h1>
<p>This is my component.</p>
</div>
);
};
// Render the component to HTML
const html = ReactDOMServer.renderToString(<MyComponent />);
// Extract the DOM Tree from the HTML and visualize it
const parser = new DOMParser();
const dom = parser.parseFromString(html, "text/html");
const tree = dom.body.firstChild;
console.log(tree);
- We could render a component into static markup using the ReactDOMServer object.
- However, components are also rendered as regular tags, so it was necessary to perform additional work to extract the component names separately.
function buildTree(components) {
const tree = {
name: "App",
children: [],
}
React.Children.forEach(components, (component) => {
if (React.isValidElement(component)) {
const child = {
name: component.type.name,
children: buildTree(component.props.children),
};
tree.children.push(child);
}
});
return tree.children.length ? tree.children : null;
}
- I was able to extract the names of the components and visualize the hierarchy.
- However, it was limited to the components declared within the
return
statement of the App component.
(Regular tags were not displayed.) - Additionally, extra logic was needed for handling conditional rendering.
-
I was able to parse the DOM tree and generate a tree structure using the react-d3-tree package.
-
However, the main logic of the project became heavily dependent on the package, which led to a lack of technical challenges and made customization difficult.
Code
import React, { useState, useEffect } from "react"; import Tree from "react-d3-tree"; function DOMTree() { const [treeData, setTreeData] = useState({}); useEffect(() => { // Assign the root element of the DOM tree to a variable const rootElement = document.documentElement; // Parse the DOM tree and create the tree data structure function createTree(node) { const tree = {}; tree.name = node.tagName.toLowerCase(); tree.children = []; for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; const childTree = createTree(child); tree.children.push(childTree); } return tree; } const treeData = createTree(rootElement); setTreeData(treeData); }, []); return ( <div id="treeWrapper" style={{ width: "159%", height: "100vh" }}> <Tree data={treeData} /> </div> ); }
- Instead of parsing the code, we use the information of the currently rendered components on the screen.
-> Utilizing React Fiber - The rendered screen on localhost is displayed in the Electron view by running
npm start
from the user's local directory.
-> Utilizingchild_process
in an Electron app
- React Fiber is a new reconciliation algorithm that restructured React's core algorithm in React v16.
- It complements the drawbacks of the old stack reconciler, which executed all tasks synchronously, allowing for concurrency.
- By assigning priorities to certain tasks, it allows parts of the task to be paused and resumed concurrently, enabling incremental rendering.
- A fiber is a JavaScript object that contains information about a component and its inputs and outputs.
- It consists of two tree structures:
current
andworkInProgress
. - Each
fiberNode
has a single linked list structure, pointing to its next node viareturn
,child
, andsibling
pointers.
- In order to visualize the component structure using
d3.js
, the data needed to be structured in a tree format. - To transform the fiber, which is in the form of a linked list, into a tree structure, I created a recursive function
createNode()
to extract the component tree structure data.import TreeNode from "./TreeNode"; const skipTag = [7, 9, 10, 11]; function createNode(fiberNode, parentTree) { if (!fiberNode || Object.keys(fiberNode).length === 0) return null; const node = fiberNode.alternate ? fiberNode.alternate : fiberNode; const tree = new TreeNode(); tree.setName(node); tree.addProps(node); tree.addState(node); if (fiberNode.sibling) { while (skipTag.includes(fiberNode.sibling.tag)) { const originSibling = fiberNode.sibling.sibling; fiberNode.sibling = fiberNode.sibling.child; fiberNode.sibling.sibling = originSibling; } const siblingTree = createNode(fiberNode.sibling, parentTree); if (siblingTree) { parentTree.addChild(siblingTree); } } if (fiberNode.child) { while (skipTag.includes(fiberNode.child.tag)) { fiberNode.child = fiberNode.child.child; } const childTree = createNode(fiberNode.child, tree); tree.addChild(childTree); } return tree; }
- Each node is classified by its
tag
, and during the data extraction process, nodes that were unnecessary for visualizing the tree structure were excluded based on theirtag
. - There are many properties that cause circular references.
Due to this, it was not possible to executeJSON.stringify()
during the JSON file generation process, so circular reference properties were excluded.
đź”˝ Classification of fiberNode tagsReference: react Github - type WorkTagexport const FunctionComponent = 0; export const ClassComponent = 1; export const IndeterminateComponent = 2; // Before we know whether it is function or class export const HostRoot = 3; // Root of a host tree. Could be nested inside another node. export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer. export const HostComponent = 5; export const HostText = 6; export const Fragment = 7; export const Mode = 8; export const ContextConsumer = 9; export const ContextProvider = 10; export const ForwardRef = 11; export const Profiler = 12; export const SuspenseComponent = 13; export const MemoComponent = 14; export const SimpleMemoComponent = 15; export const LazyComponent = 16; export const IncompleteClassComponent = 17; export const DehydratedFragment = 18; export const SuspenseListComponent = 19; export const ScopeComponent = 21; export const OffscreenComponent = 22; export const LegacyHiddenComponent = 23; export const CacheComponent = 24; export const TracingMarkerComponent = 25; export const HostHoistable = 26; export const HostSingleton = 27;
- Registering the function we created as an npm package and utilizing it
- This method is inconvenient because the user would need to install the npm package and manually add code to the project they want to analyze.
- Utilize
Symlink
.
-
A symlink (symbolic link) is a type of file in Linux that can reference another file or directory already created, in any file system.
-
The syntax to create a symlink is as follows:
ln -s <path to the original file/folder> <path to the new link>
=> To allow the external user directory to reference internal Electron app functions, we decided to use symlink.
- Create a
symlink
using the Node.js Child processexec()
. - In the
src/index.js
file of the user directory, import thereactree()
function created through thesymlink
.
// public/Electron/ipc-handler.js
exec(
`ln -s ${userHomePath}/Desktop/reactree-frontend/src/utils/reactree.js ${filePath}/src/reactree-symlink.js`,
(error, stdout, stderr) => {
const pathError = getErrorMessage(stderr);
if (pathError) handleError(view, pathError);
},
);
const JScodes = `
// eslint-disable-next-line import/first
import reactree from "./reactree-symlink";
setTimeout(() => {
reactree(root._internalRoot);
}, 0);
`;
appendFileSync(`${filePath}/src/index.js`, JScodes);
- Assign the fiber data
json
to the<div id="root">
key value in the user code and retrieve the data by executingview.webContents.executeJavascript()
- Although the functionality worked, I concluded that this approach was impractical due to data size limitations and maintainability issues.
I discovered that services built with electron
, like VScode and Slack, download necessary data as json
files locally.
Inspired by this, I decided to proceed by downloading the component tree structure data of the user project as a json
file and reading that file to visualize the component structure.
-
When the user project is run in development mode via
exec()
, thereactree()
function, referenced via symlink inindex.js
, is executed.// public/Electron/ipc-handler.js execSync(`lsof -i :${portNumber} | grep LISTEN | awk '{print $2}' | xargs kill`); exec( `PORT=${portNumber} BROWSER=none npm start`, { cwd: filePath }, (error, stdout, stderr) => { const startError = getErrorMessage(stderr); if (startError) handleError(view, startError); }, );
-
When the
reactree()
function is executed, thedata.json
file containing the fiber data is downloaded to the user's local storage.// reactree-frontend/src/utils/reactree.js const reactree = rootInternalRoot => { try { const fiber = deepCopy(rootInternalRoot); const fiberJson = JSON.stringify(fiber.current, getCircularReplacer); const blob = new Blob([fiberJson], { type: "text/json;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "data.json"; link.click(); setTimeout(() => { URL.revokeObjectURL(url); }, 0); return undefined; } catch (error) { return console.error(error); } };
-
The Electron
ipc-handler
reads thedata.json
file and sends the data to the renderer process.// public/Electron/ipc-handler.js await waitOn({ resources: [`${userHomePath}/Downloads/data.json`] }); const fiberFile = readFileSync( path.join(`${userHomePath}/Downloads/data.json`), ); mainWindow.webContents.send("node-to-react", JSON.parse(fiberFile));
- Initially, I created a link to download the extracted data in JSON format to the local environment with the user's project information.
- While there was no issue with smaller projects, I encountered data truncation issues when dealing with larger projects.
- After investigation, I found that the URI scheme method had data limitations, making it unsuitable for handling large amounts of data.
- Utilizing the
Blob
object
Blob
stands for binary large object. As the name suggests, it allows storing data in the form of binary objects.
-> To access Blob
data, I needed to create a URL that points to the Blob
object.
- Using
Blob
'screateObjectURL()
, I converted the given object into a URL as a DOMString. This URL is automatically revoked when the window is closed.
const blob = new Blob([fiberJson], { type: "text/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
- Create an
<a>
element and set thehref
attribute to theBlob
URL created above. Then, set thedownload
attribute to use it as a link for file downloading.
const link = document.createElement("a");
link.href = url;
link.download = "data.json";
- Once the download is complete, use
revokeObjectURL()
to invalidate theBlob
URL and release the resources that are no longer needed. This helps prevent memory leaks.
URL.revokeObjectURL(url);
- React
- Electron
- Redux, Redux-toolkit
- Styled-Component
- d3
- ESLint
- Jest, playwright
- Access to system resources
- You can directly use Node.js APIs within the webview page to access the file system, which is generally not possible in a web browser.
- Utilization of web development technologies
- Since Chromium is used in the Renderer process (frontend) and Node.js is used in the Main process (backend), existing web development technologies can be utilized.
- Cross-platform compatibility
- You can develop desktop applications that run on various operating systems such as Windows, macOS, and Linux.
- Electron [ Geonhwa 40% / Taewoo 30% / Minju 30% ]
- Allows access to the user's local storage via system resource access.
- Runs the program selected by the user through
child_process
. - Provides a secure synchronous two-way bridge in isolated contexts (
main
,renderer
).
- Symlink File Creation [ Geonhwa 50% / Taewoo 30% / Minju 20% ]
- When the correct folder is selected, it generates a
Symlink
file that allows thereactree
function to run in the user's project.
- When the correct folder is selected, it generates a
- Component Hierarchy Data Extraction [ Geonhwa 20% / Taewoo 40% / Minju 40% ]
- Using the recursive function
createNode()
, the component hierarchy data is extracted from thefiber
object into a tree structure.
- Using the recursive function
- Downloading the Tree Structure Data [ Geonhwa 40% / Taewoo 40% / Minju 20% ]
- When the
Select Folder
button is clicked, a prompt appears asking for consent to downloaddata.json
, and the user's code is rendered in the Electron view (development mode rendering screen). - Then,
data.json
is immediately downloaded, the tree structure is rendered based on it, anddata.json
is deleted.
- When the
- Visualizing the Component Hierarchy [ Geonhwa 30% / Taewoo 20% / Minju 50% ]
- In the tree structure, a
fiberNode
with a tag of 0 represents aFunctionComponent
, which is displayed in blue. - Scrolling the mouse zooms in/out of the tree structure, and dragging moves the tree structure according to the cursor position.
- Adjusting the WIDTH/HEIGHT slider bar at the top of the tree structure shrinks or enlarges the tree structure horizontally/vertically.
- In the tree structure, a
- Tree Structure Modal [ Geonhwa 20% / Taewoo 30% / Minju 50% ]
- When hovering over a node in the tree structure, a modal appears showing component information.
- The modal, which initially appears on the right side of the cursor, will switch to the left if the cursor is too close to the right edge, depending on the width of the Electron window and modal.
- The modal shows the component's name,
props
,local state
, andredux state
.
- Code Viewer [ Geonhwa 40% / Taewoo 20% / Minju 40% ]
- Clicking on a node in the tree displays the path and code of the JS file where the component is rendered.
- If a non-component node is clicked or the X button is clicked, the code viewer and path information disappear.
- Folder Selection [ Geonhwa 30% / Taewoo 50% / Minju 20% ]
- If the wrong folder is selected, an error popup and error page are rendered.
- Clicking the folder selection button again allows the user to load a new project and render its tree structure.
Project Duration: March 6, 2023 (Mon) ~ March 30, 2023 (Thu)
- Week 1: Idea planning and mockup creation
- Week 2-3: Feature development
- Week 4: Writing test code, presentation
- Geonhwa Lee - [email protected]
- Taewoo Kim - [email protected]
- Minju Park - [email protected]