Based on big-react,I am going to implement React v18 core features from scratch using WASM and Rust.
Code Repository:https://github.com/ParadeTo/big-react-wasm
The tag related to this article:v9
A mature and stable man library like big-react-wasm
definitely needs unit tests. So, in this article, we will pause the feature development for now and add unit tests to big-react-wasm
. The goal this time is to run the 17 test cases provided by the react
official documentation for ReactElement
.
Since the test case code runs in a Node environment, we need to modify our build output. First, let's add a new script:
"build:test": "node scripts/build.js --test",
Next, let's add handling for --test
in our build.js
file. There are two main points to consider. First, we need to change the output target of wasm-pack
to nodejs
:
execSync(
`wasm-pack build packages/react --out-dir ${cwd}/dist/react --out-name jsx-dev-runtime ${
isTest ? '--target nodejs' : ''
}`
)
In react-dom/index.js
, the statement that imports updateDispatcher
from react
needs to be changed to the commonjs
format:
isTest
? 'const {updateDispatcher} = require("react");\n'
: 'import {updateDispatcher} from "react";\n'
After setting up the Jest environment, we'll copy the ReactElement-test.js
file from big-react
and modify the module import paths:
// ReactElement-test.js
React = require('../../dist/react')
ReactDOM = require('../../dist/react-dom')
ReactTestUtils = require('../utils/test-utils')
// test-utils.js
const ReactDOM = require('../../dist/react-dom')
exports.renderIntoDocument = (element) => {
const div = document.createElement('div')
return ReactDOM.createRoot(div).render(element)
}
When executing jest
, you may notice that several test cases fail due to the following issues:
- Type of
REACT_ELEMENT_TYPE
Since REACT_ELEMENT_TYPE
in big-react-wasm
is of type string, we need to modify these test cases accordingly:
it('uses the fallback value when in an environment without Symbol', () => {
expect((<div />).$$typeof).toBe('react.element')
})
This difference will also affect the execution of the following test case:
const jsonElement = JSON.stringify(React.createElement('div'))
expect(React.isValidElement(JSON.parse(jsonElement))).toBe(false)
The reason is that the normal value of $$typeof
is of type Symbol
. Therefore, when ReactElement
is JSON.stringify
-ed, this property gets removed. In React.isValidElement
, it checks whether $$typeof
is equal to REACT_ELEMENT_TYPE
, resulting in false
as the output. However, in big-react-wasm
, REACT_ELEMENT_TYPE
is a string, so the result is true
.
Why not change it to Symbol
then? Well, Rust has many restrictions in place to ensure thread safety, making it cumbersome to define a constant of type Symbol
. Let me provide an example given by ChatGPT to illustrate this:
use wasm_bindgen::prelude::*;
use js_sys::Symbol;
use std::sync::Mutex;
pub static REACT_ELEMENT_TYPE: Mutex<Option<Symbol>> = Mutex::new(None);
// Somewhere in your initialization code, you would set the symbol:
fn initialize() {
let mut symbol = REACT_ELEMENT_TYPE.lock().unwrap();
*symbol = Some(Symbol::for_("react.element"));
}
// And when you need to use the symbol, you would lock the Mutex to safely access it:
fn use_symbol() {
let symbol = REACT_ELEMENT_TYPE.lock().unwrap();
if let Some(ref symbol) = *symbol {
// Use the symbol here
}
}
- Object without a prototype
The following test case creates an object without a prototype using Object.create
. In JavaScript, it is possible to iterate over the keys of this object.
However, when calling config.dyn_ref::<Object>()
to convert it to an Object
in Rust, it returns None
. But when calling config.is_object()
, the result is indeed true
.
it('does not fail if config has no prototype', () => {
const config = Object.create(null, {foo: {value: 1, enumerable: true}})
const element = React.createElement(ComponentFC, config)
console.log(element)
expect(element.props.foo).toBe(1)
})
So, for this situation, we can simply use the original config
as the props
:
Reflect::set(&react_element, &"props".into(), &config).expect("props panic");
-
react-dom
Host Config
In the original implementation of react-dom
's HostConfig
, an error occurs if the window
object does not exist:
fn create_text_instance(&self, content: String) -> Rc<dyn Any> {
let window = window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
Rc::new(Node::from(document.create_text_node(content.as_str())))
}
So, we need to make some modifications:
fn create_text_instance(&self, content: String) -> Rc<dyn Any> {
match window() {
None => {
log!("no global `window` exists");
Rc::new(())
}
Some(window) => {
let document = window.document().expect("should have a document on window");
Rc::new(Node::from(document.create_text_node(content.as_str())))
}
}
}
But wait, why doesn't big-react throw an error? It's because big-react specifies the testing environment as jsdom
. According to the official documentation, jsdom
is a pure JavaScript implementation of the web standards, specifically designed for Node.js, including the WHATWG DOM and HTML standards.
module.exports = {
testEnvironment: 'jsdom',
}
If that's the case, why doesn't big-react-wasm
work in the same jsdom
environment? After studying the source code, I found that when window()
is called, it actually executes the following code:
js_sys::global().dyn_into::<Window>().ok()
In the code snippet, when dyn_into::<Window>()
is called, it uses instanceof
to check if the current object is a Window
. Could this be the reason? Let's experiment by adding a code snippet like this to the test cases:
console.log(window instanceof Window)
The result is false
, surprisingly. It seems to be a bug in jsdom
. Let's search on GitHub and indeed, we found an issue related to this. Moreover, someone has already provided a solution:
// jest-config.js
module.exports = {
setupFilesAfterEnv: ['<rootDir>/setup-jest.js'],
}
// setup-jest.js
Object.setPrototypeOf(window, Window.prototype)
Let's add that solution and revert the Host Config back to its original state.
With these changes, all 17 test cases pass successfully.
Please kindly give me a star!