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.
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 = 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.
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_update(&self, fiber: Rc<RefCell<FiberNode>>);
Then, implement this trait in the react-dom library as follows:
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:
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:
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”:
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.
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:
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:
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 ;
+ 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:
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!