Implement React v18 from Scratch Using WASM and Rust - [23] Support Fragment

paradeto

ayou

Posted on August 13, 2024

Implement React v18 from Scratch Using WASM and Rust - [23] Support Fragment

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

Fragment is also a basic feature in React, so the WASM version needs to support it as well. But first, let's fix a few critical bugs.

Bug 1: In the following example, only the first click has an effect (updates to 1), and subsequent clicks keep it at 1.



function App() {
  const [num, setNum] = useState(0)
  return <div onClick={() => setNum((prev) => prev + 1)}>{num}</div>
}


Enter fullscreen mode Exit fullscreen mode

The issue lies in the update of new_base_state in update_queue.rs. It needs to be modified as follows:



-   new_base_state = result.memoized_state.clone();
+   new_base_state = new_state.clone();


Enter fullscreen mode Exit fullscreen mode

After fixing the above bug, there is still an issue related to useState.

Bug 2: In the following example, only the first click has an effect (updates to 1), and subsequent clicks keep it at 1.



function App() {
  const [num, setNum] = useState(0)
  return <div onClick={() => setNum(num + 1)}>{num}</div>
}


Enter fullscreen mode Exit fullscreen mode

After investigation, it was found that the num inside the onClick function always remains 0, even after the first click when num should be 1. The root cause is that the onClick of the div always references the function passed during the initial rendering, and the captured closure's num is also the initial 0.

Upon reviewing the code, it was discovered that we missed the update logic for props of HostComponent type FiberNode. We only handled the HostText type before. Let's fill in this part.

First, let's redefine HostConfig by removing commit_text_update and adding commit_update:



-    fn commit_text_update(&self, text_instance: Rc<dyn Any>, content: &JsValue);
+    fn commit_update(&self, fiber: Rc<RefCell<FiberNode>>);


Enter fullscreen mode Exit fullscreen mode

Then, implement this trait in the react-dom library as follows:



fn commit_update(&self, fiber: Rc<RefCell<FiberNode>>) {
  let instance = FiberNode::derive_state_node(fiber.clone());
  let memoized_props = fiber.borrow().memoized_props.clone();
  match fiber.borrow().tag {
      WorkTag::HostText => {
          let text = derive_from_js_value(&memoized_props, "content");
          self.commit_text_update(instance.unwrap(), &text);
      }
      WorkTag::HostComponent => {
          update_fiber_props(
              instance
                  .unwrap()
                  .downcast::<Node>()
                  .unwrap()
                  .dyn_ref::<Element>()
                  .unwrap(),
              &memoized_props,
          );
      }
      _ => {
          log!("Unsupported update type")
      }
  };
}


Enter fullscreen mode Exit fullscreen mode

Here, update_fiber_props already exists, and its purpose is to update the latest props onto the corresponding Element of the FiberNode.

Next, add the following code in complete_work.rs:



WorkTag::HostComponent => {
        if current.is_some() && work_in_progress_cloned.borrow().state_node.is_some() {
          // todo: compare props to decide if need to update
+         CompleteWork::mark_update(work_in_progress.clone());


Enter fullscreen mode Exit fullscreen mode

This marks the FiberNode with the Update flag. Further optimization can be done here (comparing the previous and current props to decide whether to mark the update flag), but for simplicity, let's skip it for now.

Finally, modify the handling of Update in commit_work.rs:



if flags.contains(Flags::Update) {
  unsafe {
      HOST_CONFIG
          .as_ref()
          .unwrap()
          .commit_update(finished_work.clone())
  }
  finished_work.borrow_mut().flags -= Flags::Update;
}


Enter fullscreen mode Exit fullscreen mode

The PR for fixing the bugs can be found here. With the bug fixes completed, let's proceed to implement Fragment.

First, Fragment is exported as a constant from react. However, when we try to write it as follows in Rust, an error occurs: "#[wasm_bindgen] can only be applied to a function, struct, enum, impl, or extern block":



#[wasm_bindgen]
pub static Fragment: &str = "react.fragment";


Enter fullscreen mode Exit fullscreen mode

It seems that exporting strings from Rust to JavaScript is not supported. Therefore, we need to continue modifying the compiled output through the build script, specifically by adding the code to export Fragment in the final JS file.



// add Fragment
const reactIndexFilename = `${cwd}/dist/react/jsx-dev-runtime.js`
const reactIndexData = fs.readFileSync(reactIndexFilename)
fs.writeFileSync(
  reactIndexFilename,
  reactIndexData + `export const Fragment='react.fragment';\n`
)
const reactTsIndexFilename = `${cwd}/dist/react/jsx-dev-runtime.d.ts`
const reactTsIndexData = fs.readFileSync(reactTsIndexFilename)
fs.writeFileSync(
  reactTsIndexFilename,
  reactTsIndexData + `export const Fragment: string;\n`
)


Enter fullscreen mode Exit fullscreen mode

Next, we need to add a method create_fiber_from_fragment in fiber.rs:



pub fn create_fiber_from_fragment(elements: JsValue, key: JsValue) -> FiberNode {
  FiberNode::new(WorkTag::Fragment, elements, key, JsValue::null())
}


Enter fullscreen mode Exit fullscreen mode

Here, elements refers to the children of the fragment.

Then, following the workflow, we need to add handling for Fragment in begin_work.rs:



pub fn begin_work(
    work_in_progress: Rc<RefCell<FiberNode>>,
    render_lane: Lane,
) -> Result<Option<Rc<RefCell<FiberNode>>>, JsValue> {
  ...
  return match tag {
    ...
    WorkTag::Fragment => Ok(update_fragment(work_in_progress.clone())),
  };
}

fn update_fragment(work_in_progress: Rc<RefCell<FiberNode>>) -> Option<Rc<RefCell<FiberNode>>> {
    let next_children = work_in_progress.borrow().pending_props.clone();
    reconcile_children(work_in_progress.clone(), Some(next_children));
    work_in_progress.borrow().child.clone()
}


Enter fullscreen mode Exit fullscreen mode

In the reconcile_single_element function, we also need to add handling for Fragment:



-    let mut fiber = FiberNode::create_fiber_from_element(element);
+    let mut fiber ;
+    if derive_from_js_value(&element, "type") == REACT_FRAGMENT_TYPE {
+        let props = derive_from_js_value(&element, "props");
+        let children = derive_from_js_value(&props, "children");
+        fiber = FiberNode::create_fiber_from_fragment(children, key);
+    } else {
+        fiber = FiberNode::create_fiber_from_element(element);
+    }


Enter fullscreen mode Exit fullscreen mode

This way, our React implementation can support Fragment.

However, there is another case that needs to be supported, for example:



function App() {
  const arr = [<span>Hello</span>, <span>World</span>]
  return <div>{arr}</div>
}


Enter fullscreen mode Exit fullscreen mode

In the above example, Fragment is not explicitly used, but we still need to add a layer of handling, as shown below:

Image description

This mainly involves modifying the update_from_map function in child_fiber.rs. For more details, please refer to here.

Please kindly give me a star!

💖 💪 🙅 🚩
paradeto
ayou

Posted on August 13, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related