Implement React v18 from Scratch Using WASM and Rust - [3] Reconciler & Renderer Design
Tags: react, webassembly, rust
Published: 2024-04-14 00:48:39


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


# Introduction


In the previous article, the plan was to implement the render phase in this article. However, considering the amount of content, it is better to separate it into multiple parts. Calling it "architecture design" may be a bit exaggerated. The main goal is to decouple the Renderer and Reconciler to enable the Reconciler to support different Renderer implementations. This is necessary because we will need to implement the ReactNoop Renderer for running test cases in the future.

# Reconciler


To achieve our goal in Rust, traits are indispensable. Therefore, we first define the `HostConfig` trait in the `Reconciler` to lay the foundation:

```rust
// react-reconciler/src/lib.rs
pub trait HostConfig {
    fn create_text_instance(&self, content: String) -> Rc<dyn Any>;
    fn create_instance(&self, _type: Rc<dyn Any>) -> Rc<dyn Any>;
    fn append_initial_child(&self, parent: Rc<dyn Any>, child: Rc<dyn Any>);
    fn append_child_to_container(&self, child: Rc<dyn Any>, parent: Rc<dyn Any>);
}
```

Here, only a few methods are defined, and it's important to note that the `stateNode` under different `Renderers` can have different types. To handle this uncertainty, the `std::any::Any` type is used.


Next, let's define the `Reconciler`:

```rust
// react-reconciler/src/lib.rs
pub struct Reconciler {
    host_config: Box<dyn HostConfig>,
}

impl Reconciler {
    pub fn new(host_config: Box<dyn HostConfig>) -> Self {
        Reconciler { host_config }
    }
    pub fn create_container(&self, container: &JsValue) -> Rc<RefCell<FiberRootNode>> {
        Rc::new(RefCell::new(FiberRootNode {}))
    }

    pub fn update_container(&self, element: Rc<JsValue>, root: Rc<RefCell<FiberRootNode>>) {
        log!("{:?} {:?}", element, root)
    }
}
```

In the `Reconciler` struct, the `host_config` property is included and uses a Trait Object to represent the generic type. When instantiating a `Reconciler` object within a specific `Renderer`, an instance of a type that implements the `HostConfig` trait needs to be passed.

Let's implement the other two methods quickly for debugging purposes. Next, we'll take a look at the `Renderer`.

# Renderer


The first thing in Renderer is to implement the `HostConfig`.

```rust
// react-dom/src/host_config.rs
use react_reconciler::HostConfig;


impl HostConfig for ReactDomHostConfig {
    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(document.create_text_node(content.as_str()))
    }

    fn create_instance(&self, _type: String) -> Rc<dyn Any> {
        let window = window().expect("no global `window` exists");
        let document = window.document().expect("should have a document on window");
        match document.create_element(_type.as_ref()) {
            Ok(element) => Rc::new(element),
            Err(_) => todo!(),
        }
    }

    fn append_initial_child(&self, parent: Rc<dyn Any>, child: Rc<dyn Any>) {
        let parent = parent.clone().downcast::<Element>().unwrap();
        let child = child.downcast::<Text>().unwrap();
        match parent.append_child(&child) {
            Ok(_) => {
                log!("append_initial_child successfully ele {:?} {:?}", parent, child);
            }
            Err(_) => todo!(),
        }
    }

    fn append_child_to_container(&self, child: Rc<dyn Any>, parent: Rc<dyn Any>) {
        todo!()
    }
}
```

As we can see, we can use `downcast` to convert the `Any` type into a specific type. Here, we have implemented the method in a simple manner for now.

Next, let's define a `Renderer`:

```rust
// react-dom/src/renderer.rs
#[wasm_bindgen]
pub struct Renderer {
    root: Rc<RefCell<FiberRootNode>>,
    reconciler: Reconciler,
}

impl Renderer {
    pub fn new(root: Rc<RefCell<FiberRootNode>>, reconciler: Reconciler) -> Self {
        Self { root, reconciler }
    }
}

#[wasm_bindgen]
impl Renderer {
    pub fn render(&self, element: &JsValue) {
        self.reconciler.update_container(Rc::new(element.clone()), self.root.clone())
    }
}
```


The `Renderer` includes two properties, `root` and `reconciler`. The root is generated by the `create_container` method of the `Reconciler` when `create_root` is called.

```rust
#[wasm_bindgen(js_name = createRoot)]
pub fn create_root(container: &JsValue) -> Renderer {
    set_panic_hook();
    let reconciler = Reconciler::new(Box::new(ReactDomHostConfig));
    let root = reconciler.create_container(container);
    let renderer = Renderer::new(root, reconciler);
    renderer
}
```


# Testing


Everything is ready, let's add some code to debug. We'll modify the example in `hello-world` as follows:

```js
import {createRoot} from 'react-dom'

const comp = <div>hello world</div>
const root = createRoot(document.getElementById('root'))
root.render(comp)
```

Then, in the `Reconciler`, let's start by implementing the initial rendering with hardcoded content:

```rust
pub fn update_container(&self, element: Rc<JsValue>, root: Rc<RefCell<FiberRootNode>>) {
    let props = Reflect::get(&*element, &JsValue::from_str("props")).unwrap();
    let _type = Reflect::get(&*element, &JsValue::from_str("type")).unwrap();
    let children = Reflect::get(&props, &JsValue::from_str("children")).unwrap();

    let text_instance = self.host_config.create_text_instance(children.as_string().unwrap());
    let div_instance = self.host_config.create_instance(_type.as_string().unwrap());
    self.host_config.append_initial_child(div_instance.clone(), text_instance);

    let window = window().unwrap();
    let document = window.document().unwrap();
    let body = document.body().expect("document should have a body");

    body.append_child(&*div_instance.clone().downcast::<Element>().unwrap());
}
```


If everything goes well, rebuild and install the dependencies. You should be able to see the content in the browser when running the hello world project.