Implement React v18 from Scratch Using WASM and Rust - [15] Implement useEffect
Tags: react, webassembly, rust
Published: 2024-06-04 02:38:26


> Based on [big-react](https://github.com/BetaSu/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：[v15](https://github.com/ParadeTo/big-react-wasm/tree/v15)

The details of this update can be seen [here](https://github.com/ParadeTo/big-react-wasm/pull/14/files). Let's go through the entire process below.

Like `useState`, we first need to export this method from the `react` package. It takes two parameters:

```rust
#[wasm_bindgen(js_name = useEffect)]
pub unsafe fn use_effect(create: &JsValue, deps: &JsValue) {
    let use_effect = &CURRENT_DISPATCHER.current.as_ref().unwrap().use_effect;
    use_effect.call2(&JsValue::null(), create, deps);
}
```


Next, we need to implement `mount_effect` and `update_effect` for the initial render and updates, respectively. `mount_effect` adds a new `Hook` node to the linked list of Hooks on the `FiberNode`, with its `memoized_state` property pointing to an `Effect` object. This object is also added to the `update_queue` on the `FiberNode`, which is a circular queue. Additionally, the `FiberNode` is marked with `PassiveEffect`:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jwrpps2afpgz7vozgfks.png)


The work of `update_effect` is similar to `mount_effect`, updating the `Effect` node, but it performs a shallow comparison of the incoming `deps` with the previous `prev_deps`. If they are all the same, it will not mark the `FiberNode` with `PassiveEffect`.

The properties included in `Effect` are as follows:

```rust
pub struct Effect {
  pub tag: Flags, 
  pub create: Function, 
  pub destroy: JsValue, 
  pub deps: JsValue, 
  pub next: Option<Rc<RefCell<Effect>>>,
}
```

During the Render phase, no changes are needed. In the Commit phase, we need to add logic to handle `useEffect` before `commit_mutation_effects`:

```rust
// useEffect
let root_cloned = root.clone();
let passive_mask = get_passive_mask();
if flags.clone() & passive_mask.clone() != Flags::NoFlags
    || subtree_flags.clone() & passive_mask != Flags::NoFlags
{
    if unsafe { !ROOT_DOES_HAVE_PASSIVE_EFFECTS } {
        unsafe { ROOT_DOES_HAVE_PASSIVE_EFFECTS = true }
        let closure = Closure::wrap(Box::new(move || {
            flush_passive_effects(root_cloned.borrow().pending_passive_effects.clone());
        }) as Box<dyn Fn()>);
        let function = closure.as_ref().unchecked_ref::<Function>().clone();
        closure.forget();
        unstable_schedule_callback_no_delay(Priority::NormalPriority, function);
    }
}
```

Here, we use the `scheduler` implemented in the previous article to schedule a task to execute the `flush_passive_effects` method:

```rust
fn flush_passive_effects(pending_passive_effects: Rc<RefCell<PendingPassiveEffects>>) {
    unsafe {
        if EXECUTION_CONTEXT
            .contains(ExecutionContext::RenderContext | ExecutionContext::CommitContext)
        {
            log!("Cannot execute useEffect callback in React work loop")
        }

        for effect in &pending_passive_effects.borrow().unmount {
            CommitWork::commit_hook_effect_list_destroy(Flags::Passive, effect.clone());
        }
        pending_passive_effects.borrow_mut().unmount = vec![];

        for effect in &pending_passive_effects.borrow().update {
            CommitWork::commit_hook_effect_list_unmount(
                Flags::Passive | Flags::HookHasEffect,
                effect.clone(),
            );
        }
        for effect in &pending_passive_effects.borrow().update {
            CommitWork::commit_hook_effect_list_mount(
                Flags::Passive | Flags::HookHasEffect,
                effect.clone(),
            );
        }
        pending_passive_effects.borrow_mut().update = vec![];
    }
}
```

The `pending_passive_effects` here is a property on the `FiberRootNode`, used to store the `Effect` that needs to be executed this time:

```rust
pub struct PendingPassiveEffects {
    pub unmount: Vec<Rc<RefCell<Effect>>>,
    pub update: Vec<Rc<RefCell<Effect>>>,
}
```

Among them, the `Effect` that needs to be handled due to component unmounting is saved in `unmount`, and the `Effect` that needs to be handled due to updates is saved in `update`. From the code, we can see that the `Effect` due to component unmounting is handled first, even if the component is later in the sequence, like in this example:

```js
function App() {
  const [num, updateNum] = useState(0)
  return (
    <ul
      onClick={(e) => {
        updateNum((num: number) => num + 1)
      }}>
      <Child1 num={num} />
      {num === 1 ? null : <Child2 num={num} />}
    </ul>
  )
}
function Child1({num}: {num: number}) {
  useEffect(() => {
    console.log('child1 create')
    return () => {
      console.log('child1 destroy')
    }
  }, [num])
  return <div>child1 {num}</div>
}

function Child2({num}: {num: number}) {
  useEffect(() => {
    console.log('child2 create')
    return () => {
      console.log('child2 destroy')
    }
  }, [num])
  return <div>child2 {num}</div>
}
```

After clicking, the `destroy` of `Child2`'s `useEffect` will be executed first, printing `child2 destroy`. But if it's changed to this:

```js
function App() {
  const [num, updateNum] = useState(0)
  return (
    <ul
      onClick={(e) => {
        updateNum((num: number) => num + 1)
      }}>
      <Child1 num={num} />
      <Child2 num={num} />
    </ul>
  )
}
```

After clicking, the `destroy` of `Child1`'s `useEffect` will be executed first, printing `child1 destroy`.


So when are the `Effect` in `pending_passive_effects` added? The answer is in `commit_mutation_effects`, there are two situations:


1. If the `FiberNode` node is marked for deletion and is of the `FunctionComponent` type, then the `Effect` in the `update_queue` needs to be added to the `unmount` list in `pending_passive_effects`.

```rust
fn commit_deletion(
    &self,
    child_to_delete: Rc<RefCell<FiberNode>>,
    root: Rc<RefCell<FiberRootNode>>,
) {
  let first_host_fiber: Rc<RefCell<Option<Rc<RefCell<FiberNode>>>>> =
      Rc::new(RefCell::new(None));
  self.commit_nested_unmounts(child_to_delete.clone(), |unmount_fiber| {
      let cloned = first_host_fiber.clone();
      match unmount_fiber.borrow().tag {
          WorkTag::FunctionComponent => {
              CommitWork::commit_passive_effect(
                  unmount_fiber.clone(),
                  root.clone(),
                  "unmount",
              );
          }
          ...
      }
  }
}
```


2. If the `FiberNode` node is marked with `PassiveEffect`, then the `Effect` in the `update_queue` needs to be added to the `update` list in `pending_passive_effects`.

```rust
if flags & Flags::PassiveEffect != Flags::NoFlags {
  CommitWork::commit_passive_effect(finished_work.clone(), root, "update");
  finished_work.borrow_mut().flags -= Flags::PassiveEffect;
}
```

The general process is now complete, for more details please refer to [here](https://github.com/ParadeTo/big-react-wasm/pull/14/files).