Implement React v18 from Scratch Using WASM and Rust - [7] Support FunctionComponent Type

ayou - Apr 23 - - Dev Community

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()
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}

Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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();
}

Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The expression fn1()?; is equivalent to:

match fn1() {
    Ok(return_value) => {
        return_value
    }
    Err(e) => {
        return Err(e.into())
    }
};
Enter fullscreen mode Exit fullscreen mode

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;
          }
      };
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

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),
    };
}
Enter fullscreen mode Exit fullscreen mode

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())?;
    ...
}
Enter fullscreen mode Exit fullscreen mode

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
}
...
Enter fullscreen mode Exit fullscreen mode

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 />)
Enter fullscreen mode Exit fullscreen mode

Rebuild and install the dependencies. After running it, you will see the following result:

Image description

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Image description

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player