DEV Community

Cover image for Implement React v18 from Scratch Using WASM and Rust - [25] Suspense(2) - Data Fetching with use hook
ayou
ayou

Posted on

Implement React v18 from Scratch Using WASM and Rust - [25] Suspense(2) - Data Fetching with use hook

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

In the new version of React, Suspense plays a significant role in combining with use to fetch data. Today, let's implement it. You can find the changes in this link.

Let's explain the changes using the following example:

import { Suspense, use } from 'react'

const delay = (t) =>
  new Promise((resolve, reject) => {
    setTimeout(reject, t)
  })

const cachePool = []

function fetchData(id, timeout) {
  const cache = cachePool[id]
  if (cache) {
    return cache
  }
  return (cachePool[id] = delay(timeout).then(() => {
    return { data: Math.random().toFixed(2) * 100 }
  }))
}

export default function App() {
  return (
    <Suspense fallback={<div>loading</div>}>
      <Child />
    </Suspense>
  )
}

function Child() {
  const { data } = use(fetchData(1, 1000))
  return <span>{data}</span>
}
Enter fullscreen mode Exit fullscreen mode

First, we add the relevant code according to the process of adding new hooks. Finally, we arrive at fiber_hooks.rs:

fn _use(usable: JsValue) -> Result<JsValue, JsValue> {
  if !usable.is_null() && type_of(&usable, "object") {
      if derive_from_js_value(&usable, "then").is_function() {
          return track_used_thenable(usable);
      } else if derive_from_js_value(&usable, "$typeof") == REACT_CONTEXT_TYPE {
          return Ok(read_context(usable));
      }
  }
  Err(JsValue::from_str("Not supported use arguments"))
}
Enter fullscreen mode Exit fullscreen mode

From the code, we can see that the use function can accept a Promise object or a Context object. Here, we only discuss the Promise object, so let's take a look at track_used_thenable:

#[wasm_bindgen]
extern "C" {
    pub static SUSPENSE_EXCEPTION: JsValue;
}

pub fn track_used_thenable(thenable: JsValue) -> Result<JsValue, JsValue> {
    ...
    unsafe { SUSPENDED_THENABLE = Some(thenable.clone()) };
    Err(SUSPENSE_EXCEPTION.__inner.with(JsValue::clone))
}
Enter fullscreen mode Exit fullscreen mode

We'll skip the middle part for now. In the end, it returns a variant of Result, Err, with SUSPENSE_EXCEPTION as the payload. This SUSPENSE_EXCEPTION is inserted into the result during the build process:

const SUSPENSE_EXCEPTION = new Error(
  "It's not a true mistake, but part of Suspense's job. If you catch the error, keep throwing it out"
)
Enter fullscreen mode Exit fullscreen mode

Returning SUSPENSE_EXCEPTION instead of thenable directly helps differentiate between exceptions thrown by user code and exceptions thrown by React. The value we are interested in is stored in SUSPENDED_THENABLE.

Next, we come to work_loop.rs:

loop {
    ...
    match if should_time_slice {
        work_loop_concurrent()
    } else {
        work_loop_sync()
    } {
        Ok(_) => {
            break;
        }
        Err(e) => handle_throw(root.clone(), e),
    };
}
Enter fullscreen mode Exit fullscreen mode

The e here is the SUSPENSE_EXCEPTION mentioned earlier. Let's see how handle_throw handles it:

fn handle_throw(root: Rc<RefCell<FiberRootNode>>, mut thrown_value: JsValue) {
    /*
        throw possibilities:
            1. use thenable
            2. error (Error Boundary)
    */
    if Object::is(&thrown_value, &SUSPENSE_EXCEPTION) {
        unsafe { WORK_IN_PROGRESS_SUSPENDED_REASON = SUSPENDED_ON_DATA };
        thrown_value = get_suspense_thenable();
    } else {
        // TODO
    }

    unsafe {
        WORK_IN_PROGRESS_THROWN_VALUE = Some(thrown_value);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, it checks if the exception is SUSPENSE_EXCEPTION. If it is, the actual value is retrieved, which matches what was mentioned earlier.

This value will eventually be passed to throw_and_unwind_work_loop:

    loop {
        unsafe {
            if WORK_IN_PROGRESS_SUSPENDED_REASON != NOT_SUSPENDED && WORK_IN_PROGRESS.is_some() {
                let thrown_value = WORK_IN_PROGRESS_THROWN_VALUE.clone().unwrap();

                WORK_IN_PROGRESS_SUSPENDED_REASON = NOT_SUSPENDED;
                WORK_IN_PROGRESS_THROWN_VALUE = None;

                // TODO
                mark_update_lane_from_fiber_to_root(
                    WORK_IN_PROGRESS.clone().unwrap(),
                    lane.clone(),
                );

                throw_and_unwind_work_loop(
                    root.clone(),
                    WORK_IN_PROGRESS.clone().unwrap(),
                    thrown_value,
                    lane.clone(),
                );
            }
        }
        ...
    }
Enter fullscreen mode Exit fullscreen mode

We have already discussed this in the previous article, so we won't go into detail here. Let's go back to track_used_thenable:

pub fn track_used_thenable(thenable: JsValue) -> Result<JsValue, JsValue> {
    let status = derive_from_js_value(&thenable, "status");
    if status.is_string() {
      ...
    } else {
        Reflect::set(&thenable, &"status".into(), &"pending".into());
        let v = derive_from_js_value(&thenable, "then");
        let then = v.dyn_ref::<Function>().unwrap();

        let thenable1 = thenable.clone();
        let on_resolve_closure = Closure::wrap(Box::new(move |val: JsValue| {
            if derive_from_js_value(&thenable1, "status") == "pending" {
                Reflect::set(&thenable1, &"status".into(), &"fulfilled".into());
                Reflect::set(&thenable1, &"value".into(), &val);
            }
        }) as Box<dyn Fn(JsValue) -> ()>);
        let on_resolve = on_resolve_closure
            .as_ref()
            .unchecked_ref::<Function>()
            .clone();
        on_resolve_closure.forget();

        let thenable2 = thenable.clone();
        let on_reject_closure = Closure::wrap(Box::new(move |err: JsValue| {
            if derive_from_js_value(&thenable2, "status") == "pending" {
                Reflect::set(&thenable2, &"status".into(), &"rejected".into());
                Reflect::set(&thenable2, &"reason".into(), &err);
            }
        }) as Box<dyn Fn(JsValue) -> ()>);
        let on_reject = on_reject_closure
            .as_ref()
            .unchecked_ref::<Function>()
            .clone();
        on_reject_closure.forget();

        then.call2(&thenable, &on_resolve, &on_reject);
    }
}
Enter fullscreen mode Exit fullscreen mode

When entering this function for the first time, it goes to the else branch. The core logic is to add on_resolve and on_reject methods to thenable and modify its status, value, and reason properties.

When the status of the Promise object is no longer pending, it triggers a re-render. When this function is called again, the status will have a value, and it will enter the if block:

if status.is_string() {
  if status == "fulfilled" {
      return Ok(derive_from_js_value(&thenable, "value"));
  } else if status == "rejected" {
      return Err(derive_from_js_value(&thenable, "reason"));
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

If the status is fulfilled, it returns the value; otherwise, it throws the exception from the reason field.

This is how Suspense combines with the use hook to fetch data. However, debugging revealed that the bailout logic affects the normal operation of this process. Therefore, for now, we have to temporarily comment out this part of the code and revisit how to solve it later when we have time.

Top comments (0)