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
:
pub fn begin_work(work_in_progress: Rc<RefCell<FiberNode>>) -> Option<Rc<RefCell<FiberNode>> {
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
:
impl FiberHooks {
...
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:
enum Result {
Ok(T),
Err(E)
}
We can handle the return value or thrown exceptions using match
, as shown in the following example:
fn fn1() -> Result<(), String> {
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:
fn fn1() -> Result<(), String> {
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:
match fn1() {
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:
fn perform_sync_work_on_root(&mut self, root: Rc<RefCell<FiberRootNode>>) {
...
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:
pub fn begin_work(work_in_progress: Rc<RefCell<FiberNode>>) -> Result<Option<Rc<RefCell<FiberNode>>>, JsValue> {
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:
fn perform_unit_of_work(&mut self, fiber: Rc<RefCell<FiberNode>>) -> Result<(), JsValue> {
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:
// App.tsx
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:
function App() {
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!