Implement React v18 from Scratch Using WASM and Rust - [23] Support Fragment
ayou
Posted on August 13, 2024
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>
}
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();
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>
}
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>>);
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")
}
};
}
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());
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;
}
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";
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`
)
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())
}
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()
}
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);
+ }
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>
}
In the above example, Fragment
is not explicitly used, but we still need to add a layer of handling, as shown below:
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!
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
September 26, 2024
September 20, 2024
September 3, 2024