❄️ [URFC] 01. Understanding React from Code
tags
react
URFC
type
Post
summary
status
Published
slug
urfc-01-understanding-react-from-code
date
Dec 1, 2023
New JSX TransformImplement JSX methodReactSymbolsCreate ReactSymbols and ReactTypeImplement jsx method
I plan to write a series of blog articles about understanding React from the code perspective, which I'll abbreviate as "URFC", to document my learning process. However, as a learner, mistakes are inevitable, but I will do my best to maintain the timeliness and accuracy of the articles.
New JSX Transform
Before we get to the main topic, we need to review the matter of JSX transform.
As we all know, browsers cannot understand JSX, so we need to rely on compilers like Babel to transform JSX code into regular JavaScript code. In essence, this involves calling
React.createElement(...)
to create a corresponding object
that describes the element.For example, let’s say your source code looks like this:
import React from 'react'; function App() { return <h1>Hello World</h1>; }
Under the hood, the old JSX transform turns it into regular JavaScript:
import React from 'react'; function App() { return React.createElement('h1', null, 'Hello world'); }
However, using
React.createElement
has two disadvantages:- Since JSX is compiled into
React.createElement
, you need to import React.
React.createElement
limits some performance improvements and increases code complexity.
So React 17 introduces a new way which is intended to only be used by compilers like Babel and TypeScript:
Let’s say that your source code looks like this:
function App() { return <h1>Hello World</h1>; }
This is what the new JSX transform compiles it to:
// Inserted by a compiler (don't import it yourself!) import {jsx as _jsx} from 'react/jsx-runtime'; function App() { return _jsx('h1', { children: 'Hello world' }); }
But don’t worry, the
React.createElement
is still available.Implement JSX method
There’re actually three method for jsx transform in runtime:
- jsxDEV method (for development environment)
- jsx method (for production environment)
- React.createElement method
Now we can proceed to implement them.
ReactSymbols
Firstly we need to understand what jsx method returned. To do this, we can simply do
console.log(<div>test</div>)
, the results should look like:{ $$typeof: Symbol.for('react.element'), props: { ... }, type: 'div', key: null, ref: null, // ...others }
But wait, what is the
$$typeof: Symbol.for('react.element')
?In fact, this mechanism is used by React to determine whether an “element” is truly a React element. It might sound a bit complicated, but consider this scenario:
You receive some unusual JSON data from a server or another source and convert it into an object:
const result = { type: 'div', props: { dangerouslySetInnerHTML: { __html: 'do something bad' }, }, // ... }
Then, you pass this object to an element:
<p>{result}</p>
If we do not concern ourselves with whether
result
is genuinely an element created by React, this situation can become highly vulnerable to attacks. This is because the data may have been injected with malicious code.Therefore, React decides whether to process an element based on the identifier
$$typeof: Symbol.for('react.element')
. Symbols with the same value cannot be added to data from external sources such as the server side, thus preventing the falsification of React elements.Create ReactSymbols and ReactType
The project folder structure for React is as follows:
react
(common methods independent of the host environment)
react-reconciler
(implementation of the reconciler, independent of the host environment)
shared
(common auxiliary methods, independent of the host environment)
- Packages for various host environments
The
ReactSymbols
and ReactType
we defined are located in the shared
folder.// shared/ReactSymbols.ts // Check if Symbol is supported const supportSymbol = typeof Symbol === "function" && Symbol.for; // Create or retrieve a unique symbol to represent React elements. // If Symbol.for is not supported, random number like 0xeac7 will be used instead. export const REACT_ELEMENT_TYPE = supportSymbol ? Symbol.for("react.element") : 0xeac7;
// shared/ReactType.ts export type Type = any; export type Key = any; export type Ref = any; export type Props = any; export type ElementType = any; export interface ReactElement { $$typeof: symbol | number; type: Type; key: Key; ref: Ref; props: Props; __mark: string; // just write your name 👀 }
Implement jsx method
As previously mentioned, the React source code actually implements three methods:
jsx
, jsxDEV
, and createElement
. For simplicity, we only take createElement
method here.import { REACT_ELEMENT_TYPE } from "shared/ReactSymbols"; import { Type, Key, Ref, Props, ReactElement as ReactElementType, ElementType, } from "shared/ReactType"; const ReactElement = function ( type: Type, key: Key, ref: Ref, props: Props, ): ReactElementType { const element = { $$typeof: REACT_ELEMENT_TYPE, type, key, ref, props, __mark: "stimw", }; return element; }; export const createElement = ( type: ElementType, config: any, ...maybeChildren: any ) => { let key: Key = null; const props: Props = {}; let ref: Ref = null; // process the props for (const prop in config) { const val = config[prop]; // add key and ref to separate variables if (prop === "key") { if (val != undefined) { key = "" + val; } continue; } if (prop === "ref") { if (val != undefined) { ref = val; } continue; } // add everything else to the props object // use hasOwnProperty to avoid prototype pollution if ({}.hasOwnProperty.call(config, prop)) { props[prop] = val; } } // process the children const maybeChildrenLength = maybeChildren.length; if (maybeChildrenLength === 1) { // [child1] props.children = maybeChildren[0]; } else if (maybeChildrenLength > 1) { // [child1, child2, ...] props.children = maybeChildren; } return ReactElement(type, key, ref, props); };