Implement React v18 from Scratch Using WASM and Rust - [11] Implement Event System

paradeto

ayou

Posted on May 7, 2024

Implement React v18 from Scratch Using WASM and Rust - [11] Implement Event System

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:v11

Without an event system, React lacks interactivity, and users are unable to interact with the application. Therefore, in this article, we will use the click event as an example to demonstrate how to implement it.

Let's take a look at how the official version of React handles events, using the following code as an example.

const App = () => {
  innerClick = () => {
    console.log('A: react inner click.')
  }

  outerClick = () => {
    console.log('B: react outer click.')
  }

  return (
    <div id='outer' onClickCapture={this.outerClick}>
      <button id='inner' onClick={this.innerClick}>
        button
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

When an event is triggered on the root element, we can obtain the native event object NativeEvent. By accessing the target property, we can retrieve the current clicked element, which in this case is the button. Using the property __reactFiber$***** (where * represents a random number) on the button, we can access the corresponding FiberNode. Additionally, React utilizes the NativeEvent to generate a SyntheticEvent, which has several important properties worth noting:

  • nativeEvent: Points to the NativeEvent.
  • _dispatchListeners: Stores the event listener functions to be executed.
  • _dispatchInstances: Stores the FiberNode objects to which the event listener functions belong.

Image description

Next, we will collect the event listener functions to be executed in both the capturing and bubbling phases.

Image description

Image description

Finally, the methods in _dispatchListeners are executed in order, and the corresponding stateNode is obtained from the FiberNode in _dispatchInstances as the currentTarget on the SyntheticEvent.

The SyntheticEvent also has a stopPropagation method. Once called, the methods following _dispatchListeners will not be executed, effectively preventing event propagation.

That concludes the introduction to the React event system. For more information, you can refer to this article.

However, the implementation of Big React differs from the official React. Here's how it's done:

During the complete work phase, when creating the stateNode for a FiberNode node, the event listener functions on the FiberNode node are copied to the Element.

Image description

When an event is triggered, the event listener functions are collected by traversing up the tree using the target property on the NativeEvent. If it's an onClick event, the function is pushed into the bubble list. If it's an onClickCapture event, the function is inserted into the capture list.

Image description

Then, the event listener functions in the capture list are executed from start to finish, followed by the event listener functions in the bubble list.

How is event propagation prevented? The answer lies in modifying the method on the NativeEvent.

fn create_synthetic_event(e: Event) -> Event {
    Reflect::set(&*e, &"__stopPropagation".into(), &JsValue::from_bool(false));

    let e_cloned = e.clone();
    let origin_stop_propagation = derive_from_js_value(&*e, "stopPropagation");
    let closure = Closure::wrap(Box::new(move || {
        // set __stopPropagation to true
        Reflect::set(
            &*e_cloned,
            &"__stopPropagation".into(),
            &JsValue::from_bool(true),
        );
        if origin_stop_propagation.is_function() {
            let origin_stop_propagation = origin_stop_propagation.dyn_ref::<Function>().unwrap();
            origin_stop_propagation.call0(&JsValue::null());
        }
    }) as Box<dyn Fn()>);
    let function = closure.as_ref().unchecked_ref::<Function>().clone();
    closure.forget();
    Reflect::set(&*e.clone(), &"stopPropagation".into(), &function.into());
    e
}

fn trigger_event_flow(paths: Vec<Function>, se: &Event) {
    for callback in paths {
        callback.call1(&JsValue::null(), se);
        // If __stopPropagation is true, break
        if derive_from_js_value(se, "__stopPropagation")
            .as_bool()
            .unwrap()
        {
            break;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

However, there is another issue here. The currentTarget is not corrected as it is in the official version of React. In this case, the currentTarget always refers to the root element because the event listener functions are bound to that element.

pub fn init_event(container: JsValue, event_type: String) {
  ...
  let element = container
      .clone()
      .dyn_into::<Element>()
      .expect("container is not element");
  let on_click = EventListener::new(&element.clone(), event_type.clone(), move |event| {
      dispatch_event(&element, event_type.clone(), event)
  });
  on_click.forget();
}
Enter fullscreen mode Exit fullscreen mode

This update can be found in detail here.

Please kindly give me a star

💖 💪 🙅 🚩
paradeto
ayou

Posted on May 7, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related