Implement React v18 from Scratch Using WASM and Rust - [15] Implement useEffect

ayou - Jun 4 - - 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:v15

The details of this update can be seen here. 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:

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

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

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:

pub struct Effect {
  pub tag: Flags, 
  pub create: Function, 
  pub destroy: JsValue, 
  pub deps: JsValue, 
  pub next: Option<Rc<RefCell<Effect>>>,
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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![];
    }
}
Enter fullscreen mode Exit fullscreen mode

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

pub struct PendingPassiveEffects {
    pub unmount: Vec<Rc<RefCell<Effect>>>,
    pub update: Vec<Rc<RefCell<Effect>>>,
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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

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

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.
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",
              );
          }
          ...
      }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. 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.
if flags & Flags::PassiveEffect != Flags::NoFlags {
  CommitWork::commit_passive_effect(finished_work.clone(), root, "update");
  finished_work.borrow_mut().flags -= Flags::PassiveEffect;
}
Enter fullscreen mode Exit fullscreen mode

The general process is now complete, for more details please refer to here.

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