DEV Community

Cover image for Implement React v18 from Scratch Using WASM and Rust - [4] Implementation of Begin Work Phase of Render Process
ayou
ayou

Posted on

Implement React v18 from Scratch Using WASM and Rust - [4] Implementation of Begin Work Phase of Render Process

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:v4

The update process in React can be divided into two main phases: Render and Commit. The Render phase can further be divided into two stages: begin work and complete work. This article focuses on implementing the begin work stage.

In the previous article, we discussed that when the render() method is called, it invokes the update_container method in the reconciler:



pub fn update_container(&self, element: Rc<JsValue>, root: Rc<RefCell<FiberRootNode>>) {
    let host_root_fiber = Rc::clone(&root).borrow().current.clone();
    let update = create_update(element);
    enqueue_update(host_root_fiber.borrow(), update);
    ...
}


Enter fullscreen mode Exit fullscreen mode

After executing the above code, a data structure like the following will be constructed:

Image description

The definitions of FiberRootNode and FiberNode are as follows:



pub struct FiberRootNode {
    pub container: Rc<JsValue>,
    pub current: Rc<RefCell<FiberNode>>,
    pub finished_work: Option<Rc<RefCell<FiberNode>>>,
}

pub struct FiberNode {
    pub tag: WorkTag,
    pub pending_props: Option<Rc<JsValue>>,
    key: Option<String>,
    pub state_node: Option<Rc<StateNode>>,
    pub update_queue: Option<Rc<RefCell<UpdateQueue>>>,
    pub _return: Option<Rc<RefCell<FiberNode>>>,
    pub sibling: Option<Rc<RefCell<FiberNode>>>,
    pub child: Option<Rc<RefCell<FiberNode>>>,
    pub alternate: Option<Rc<RefCell<FiberNode>>>,
    pub _type: Option<Rc<JsValue>>,
    pub flags: Flags,
    pub subtree_flags: Flags,
    pub memoized_props: Option<Rc<JsValue>>,
    pub memoized_state: Option<Rc<JsValue>>,
}


Enter fullscreen mode Exit fullscreen mode

Here, the Rc smart pointer is used to allow a value to have multiple owners, and RefCell is used for "interior mutability" for certain fields that need to be modified.

Next, we are going to build a FiberNode Tree:



pub fn update_container(&self, element: Rc<JsValue>, root: Rc<RefCell<FiberRootNode>>) {
    ...
    let mut work_loop = WorkLoop::new(self.host_config.clone());
    work_loop.schedule_update_on_fiber(host_root_fiber);
}


Enter fullscreen mode Exit fullscreen mode

Because the subsequent code is mostly a translation of the implementation in big-react from JavaScript to Rust, there is no need to go into too much detail. Here are a few differences to note.

Difference 1: workInProgress

In big-react, workInProgress is a module-level variable. However, Rust does not have the concept of module-level variables, so it has been changed to be an attribute within a struct.



pub struct WorkLoop {
    work_in_progress: Option<Rc<RefCell<FiberNode>>>,
}


Enter fullscreen mode Exit fullscreen mode

In Rust, a new struct called WorkLoop has been introduced, whereas in big-react, it was exported as a function in the work_loop.js module.

Difference 2: stateNode

In big-react, the stateNode is of type any because for the root FiberNode, its stateNode is a FiberRootNode, while for other nodes, the stateNode is a DOM object in JavaScript. In Rust, an enum is used to represent this:



pub enum StateNode {
    FiberRootNode(Rc<RefCell<FiberRootNode>>),
    Element(Rc<dyn Any>),
}


Enter fullscreen mode Exit fullscreen mode

It is a bit more cumbersome to use, as it requires the use of match for branching and handling different cases:



match fiber_node.state_node {
    None => {}
    Some(state_node) => {
        match &*state_node {
            StateNode::FiberRootNode(fiber_root_node) => {}
            StateNode::Element(ele) => {},
        };
    }
}


Enter fullscreen mode Exit fullscreen mode

Alternatively, it can be done similarly to destructuring assignment in JavaScript:



let Some(StateNode::FiberRootNode(fiber_root_node)) = fiber_node.state_node.clone();


Enter fullscreen mode Exit fullscreen mode

Difference 3: performSyncWorkOnRoot

In big-react, try-catch is used to catch any errors that occur during the workLoop process:



do {
  try {
    workLoop()
    break
  } catch (e) {
    console.error('workLoop error', e)
    workInProgress = null
  }
} while (true)


Enter fullscreen mode Exit fullscreen mode

Since Rust does not support try-catch, but instead uses Result to handle errors, we won't consider it for now and will implement it later:



loop {
  self.work_loop();
  break;
}


Enter fullscreen mode Exit fullscreen mode

Since we are currently only implementing the begin work phase, we will temporarily comment out complete_unit_of_work in perform_unit_of_work. Instead, we will assign None to work_in_progress to make the loop exit:



fn work_loop(&mut self) {
  while self.work_in_progress.is_some() {
      self.perform_unit_of_work(self.work_in_progress.clone().unwrap());
  }
}

fn perform_unit_of_work(&mut self, fiber: Rc<RefCell<FiberNode>>) {
  let next = begin_work(fiber.clone());

  if next.is_none() {
      // self.complete_unit_of_work(fiber.clone())
      self.work_in_progress = None;
  } else {
      self.work_in_progress = Some(next.unwrap());
  }
}


Enter fullscreen mode Exit fullscreen mode

Next, let's print the generated FiberNode tree in this phase to see if the results are correct:



fn perform_sync_work_on_root(&mut self, root: Rc<RefCell<FiberRootNode>>) {
  self.prepare_fresh_stack(Rc::clone(&root));

  loop {
      self.work_loop();
      break;
  }

  log!("{:?}", *root.clone().borrow());
}


Enter fullscreen mode Exit fullscreen mode

To print the FiberRootNode, we also need to implement the Debug trait for it:



impl Debug for FiberRootNode {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
    }
}


Enter fullscreen mode Exit fullscreen mode

The implementation approach is to use breadth-first traversal. You can refer to the code for more details. Now, let's modify the example in the "hello world" project:



import {createRoot} from 'react-dom'
const comp = (
  <div>
    <p>
      <span>Hello World</span>
    </p>
  </div>
)
const root = createRoot(document.getElementById('root'))
root.render(comp)


Enter fullscreen mode Exit fullscreen mode

You can see the following output:

Image description

Since the reconciliation process for children as an array has not been implemented yet, we can only test the case with a single child for now.

Please kindly give a star.

Top comments (0)