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:v7
The previous article has already implemented the initial rendering for HostComponent and HostText types. In this article, we will add support for FunctionComponent, although Hooks are not supported for now.
Following the process, the first step is to add a branch for handling FunctionComponent in begin_work:
let tag = work_in_progress.clone().borrow().tag.clone();
return match tag {
WorkTag::FunctionComponent => update_function_component(work_in_progress.clone()),
…
};
}
fn update_function_component(
work_in_progress: Rc<RefCell<FiberNode>>,
) -> Option<Rc<RefCell<FiberNode>>> {
let fiber_hooks = &mut FiberHooks::new();
let next_children = Rc::new(fiber_hooks.render_with_hooks(work_in_progress.clone())?);
reconcile_children(work_in_progress.clone(), Some(next_children));
work_in_progress.clone().borrow().child.clone()
}
From the code, we can see that we need to create a new struct called FiberHooks and implement the render_with_hooks method. Currently, the core of this method is to extract _type and pending_props from the FiberNode and call the function pointed to by _type (with pending_props as the argument) to obtain children:
…
pub fn render_with_hooks(&mut self, work_in_progress: Rc<RefCell<FiberNode>>) -> Result<JsValue, JsValue> {
…
let work_in_progress_borrow = work_in_progress_cloned.borrow();
let _type = work_in_progress_borrow._type.as_ref().unwrap();
let props = work_in_progress_borrow.pending_props.as_ref().unwrap();
let component = JsValue::dyn_ref::<Function>(_type).unwrap();
let children = component.call1(&JsValue::null(), props);
children
}
}
Since function execution can potentially throw exceptions, Rust doesn’t have a try-catch mechanism. Instead, it uses the Result<T, E> enum to handle exceptions. In this enum, T represents the return value, and E represents the thrown exception. It has two variants: Ok(T) represents a successful return, and Err(E) represents an error return:
Ok(T),
Err(E)
}
We can handle the return value or thrown exceptions using match, as shown in the following example:
Err(“a”.to_string())
}
fn my_fn() {
match fn1() {
Ok(return_value) => {
println!(“return: {:?}”, return_value)
}
Err(e) => {
println!(“error: {:?}”, e)
}
}
}
fn main() {
my_fn();
}
We can also use the ? operator to propagate errors, as shown in the following example:
Err(“a”.to_string())
}
fn my_fn() -> Result<String, String> {
fn1()?;
Ok(“my_fn succeed”.to_string())
}
fn main() {
match my_fn() {
Ok(return_value) => {
println!(“return: {:?}”, return_value)
}
Err(e) => {
println!(“error: {:?}”, e)
}
}
}
The expression fn1()?; is equivalent to:
Ok(return_value) => {
return_value
}
Err(e) => {
return Err(e.into())
}
};
Returning to our code, after executing component.call1(&JsValue::null(), props), the return type is Result<JsValue, JsValue>. We need to propagate the thrown error to perform_sync_work_on_root for handling:
…
loop {
match self.work_loop() {
Ok(_) => {
break;
}
Err(e) => {
self.work_in_progress = None;
}
};
}
…
}
Therefore, the return values of the functions between them need to be changed to Result. For example, begin_work needs to be modified as follows:
let tag = work_in_progress.clone().borrow().tag.clone();
return match tag {
WorkTag::FunctionComponent => update_function_component(work_in_progress.clone()),
WorkTag::HostRoot => Ok(update_host_root(work_in_progress.clone())),
WorkTag::HostComponent => Ok(update_host_component(work_in_progress.clone())),
WorkTag::HostText => Ok(None),
};
}
For example, perform_unit_of_work has been modified to change its return value and uses ? to propagate errors:
let next = begin_work(fiber.clone())?;
…
}
Next is complete_work. For FunctionComponent, it is straightforward. We just need to execute bubble_properties:
WorkTag::FunctionComponent => {
self.bubble_properties(work_in_progress.clone());
None
}
…
Finally, we reach the Commit phase, which doesn’t require any modifications. It looks great.
Next, let’s test it out. Here’s a demo:
import dayjs from ‘dayjs‘
function App() {
return (
<div>
<Comp>{dayjs().format()}</Comp>
</div>
)
}
function Comp({children}) {
return (
<span>
<i>{`Hello world, ${children}`}</i>
</span>
)
}
export default App
// main.tsx
import {createRoot} from ‘react-dom‘
import App from ‘./App.tsx‘
const root = createRoot(document.getElementById(‘root‘))
root.render(<App />)
Rebuild and install the dependencies. After running it, you will see the following result:
The logs and the rendered result on the page are both normal.
Next, let’s test the scenario where an exception is thrown:
return (
<div>
{/* will throw error */}
<Comp>{dayjs.format()}</Comp>
</div>
)
}
Currently, we haven’t handled the exceptions,we have only interrupted the render process.
With this, the initial rendering support for FunctionComponent is complete. Since we haven’t implemented Hooks yet, there aren’t many areas that need modification.
Please kindly give me a star!