Implement React v18 from Scratch Using WASM and Rust - [2] Implementation of ReactElement
Tags: react, webassembly, rust
Published: 2024-04-07 14:26:37

> Based on [big-react](https://github.com/BetaSu/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：[v2](https://github.com/ParadeTo/big-react-wasm/tree/v2)

# Implementation of ReactElement

In the previous article, we set up the development debugging environment. This time, we will implement the react library.

Without further ado, let's take a look at the compiled code:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1eqbgjub2nb0lh9k1d1h.png)

For now, we are only concerned with the first three parameters passed into `jsxDEV`, which are:

- `type`, representing the type of `ReactElement`. If it's an `HTMLElement`, this would be its corresponding tag (`string`). If it's a user-defined component, this would be a `function`.

- `props`, the parameters passed to the `ReactElement`, including `children`.

- `key`, this does not need much explanation, everyone knows what it is.

Following this order, let's define our `jsx_dev` function:

```rust

#[wasm_bindgen(js_name = jsxDEV)]
pub fn jsx_dev(_type: &JsValue, config: &JsValue，key: &JsValue) -> JsValue {

}
```


Here are a few points to note:

- What is JsValue, and why is the type JsValue?
  JsValue internally contains a u32 type index, which can be used to access objects in JS. More details can be found at the end of the document.

- Why isn't the return a `ReactElement` object?
  Because this `ReactElement` object will eventually be passed to react-dom, it will still only be defined as `JsValue`. Therefore, there's no need to define it as `ReactElement` here.

Implementing this method is also quite simple, just convert the incoming parameters into an object as shown below:

```
{
  $$typeof: REACT_ELEMENT_TYPE,
  type: type,
  key: key,
  ref: ref,
  props: props,
}
```


The code is as follows:

```rust
use js_sys::{Object, Reflect};
use wasm_bindgen::prelude::*;

use shared::REACT_ELEMENT_TYPE;

#[wasm_bindgen(js_name = jsxDEV)]
pub fn jsx_dev(_type: &JsValue, config: &JsValue, key: &JsValue) -> JsValue {
    // Initialize an empty object
    let react_element = Object::new();
    // Set properties of react_element using Reflect::set
    Reflect::set(
        &react_element,
        &"&&typeof".into(),
        &JsValue::from_str(REACT_ELEMENT_TYPE),
    )
    .expect("$$typeof panic");
    Reflect::set(&react_element, &"type".into(), _type).expect("_type panic");
    Reflect::set(&react_element, &"key".into(), key).expect("key panic");

    // Iterate config and copy every property to props except ref.
    // The ref property will be set to react_element
    let conf = config.dyn_ref::<Object>().unwrap();
    let props = Object::new();
    for prop in Object::keys(conf) {
        let val = Reflect::get(conf, &prop);
        match prop.as_string() {
            None => {}
            Some(k) => {
                if k == "ref" && val.is_ok() {
                    Reflect::set(&react_element, &"ref".into(), &val.unwrap()).expect("ref panic");
                } else if val.is_ok() {
                    Reflect::set(&props, &JsValue::from(k), &val.unwrap()).expect("props panic");
                }
            }
        }
    }

    // Set props of react_element using Reflect::set
    Reflect::set(&react_element, &"props".into(), &props).expect("props panic");
    // Convert Object into JsValue
    react_element.into()
}

```


For simplicity, we did not define `REACT_ELEMENT_TYPE` as `Symbol`, but `&str`:

```rust
pub static REACT_ELEMENT_TYPE: &str = "react.element";
```


It is defined in the shared project, so the `Cargo.toml` file in the react project also needs to add the code as below:

```
[dependencies]
shared = { path = "../shared" }
```


Rebuild and run the previous example, you can see the following output, that means the react library is completed:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ooiugmp6ukkd4ahcl82q.png)

This article has touched on the implementation of the react library of the React18 WASM, which is relatively simple. The difficulty will increase from now on. We know that a single update process in React is divided into two major phases: render and commit. So in the next article, we will implement the render phase.

# Supplement: Exploring the Principles of JsValue


Previously, we briefly discussed JsValue. Now, let's delve deeper into its principles. First, let's look at the code in the `jsx-dev-runtime_bg.js` file after packaging with `wasm-pack`. We find the `jsxDEV` function:

```js
export function jsxDEV(_type, config, key) {
  try {
    const ret = wasm.jsxDEV(
      addBorrowedObject(_type),
      addBorrowedObject(config),
      addBorrowedObject(key)
    )
    return takeObject(ret)
  } finally {
    heap[stack_pointer++] = undefined
    heap[stack_pointer++] = undefined
    heap[stack_pointer++] = undefined
  }
}
```

The parameters passed in are all processed by the `addBorrowedObject` method, so let's continue to look into it:

```js
const heap = new Array(128).fill(undefined);

heap.push(undefined, null, true, false);
let stack_pointer = 128;
...
function addBorrowedObject(obj) {
  if (stack_pointer == 1) throw new Error('out of js stack')
  heap[--stack_pointer] = obj
  return stack_pointer
}
```

Oh, it turns out that on the JS side, an array is used to simulate a heap structure, and the parameters are all stored on this heap. The three parameters are stored in the following way:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ykl8nh8ivorqxn1qxs2q.png)

And what's actually passed into wasm.jsxDEV is just the index of the array. So, how does the WASM side obtain the actual object through this index? For example, how does this code Reflect::get(conf, &prop); work?

If you think about it carefully, since the data is still on the JS side and only the index is passed to WASM, it's necessary that the JS side must also provide some interfaces for the WASM side to call. Continuing to look at the code in jsx-dev-runtime_bg.js, we find a method getObject(idx), which is used to retrieve data from the heap through an index:

```js
function getObject(idx) {
  return heap[idx]
}
```

So, let's set a breakpoint in this function and continue stepping through until we reach a call stack like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e14snfkcmpsjheoffijc.png)

In WASM, it shows that the method `__wbg_get_e3c254076557e348` was called:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2s7ca6n46isjpwhulcl5.png)

The method `__wbg_get_e3c254076557e348` can be found in `jsx-dev-runtime_bg.js`:

```js
export function __wbg_get_e3c254076557e348() {
  return handleError(function (arg0, arg1) {
    const ret = Reflect.get(getObject(arg0), getObject(arg1))
    return addHeapObject(ret)
  }, arguments)
}
```


At this point, the related data is as shown in the figure:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/foa3erjxk9j5lv10hhh9.png)

This corresponds to the execution of this step in the Rust code:

```rust
let val = Reflect::get(conf, &prop); // prop 为 children
```

At this point, the truth is revealed.