Implement React v18 from Scratch Using WASM and Rust - [16] Implement React Noop

paradeto

ayou

Posted on June 11, 2024

Implement React v18 from Scratch Using WASM and Rust - [16] Implement React Noop

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

The previous articles always mentioned the implementation of React Noop for unit testing, and today we will complete this task.

First, following the previous approach, we create a react-noop directory at the same level as react-dom:

├── packages
│   ├── react
│   ├── react-dom
│   ├── react-noop
│   ├── react-reconciler
│   ├── scheduler
│   └── shared
Enter fullscreen mode Exit fullscreen mode

The project structure is similar to react-dom, but the difference lies in the implementation of HostConfig in react-noop. For example, in react-dom, the create_instance function returns an Element object:

fn create_instance(&self, _type: String, props: Rc<dyn Any>) -> Rc<dyn Any> {
  let window = window().expect("no global `window` exists");
  let document = window.document().expect("should have a document on window");
  match document.create_element(_type.as_ref()) {
      Ok(element) => {
          let element = update_fiber_props(
              element.clone(),
              &*props.clone().downcast::<JsValue>().unwrap(),
          );
          Rc::new(Node::from(element))
      }
      Err(_) => {
          panic!("Failed to create_instance {:?}", _type);
      }
  }
}
Enter fullscreen mode Exit fullscreen mode

In react-noop, it returns a regular JavaScript object:

fn create_instance(&self, _type: String, props: Rc<dyn Any>) -> Rc<dyn Any> {
  let obj = Object::new();
  Reflect::set(&obj, &"id".into(), &getCounter().into());
  Reflect::set(&obj, &"type".into(), &_type.into());
  Reflect::set(&obj, &"children".into(), &**Array::new());
  Reflect::set(&obj, &"parent".into(), &JsValue::from(-1.0));
  Reflect::set(
      &obj,
      &"props".into(),
      &*props.clone().downcast::<JsValue>().unwrap(),
  );
  Rc::new(JsValue::from(obj))
}
Enter fullscreen mode Exit fullscreen mode

Other methods are also operations on regular JavaScript objects. For more details, please refer to this link.

Additionally, to facilitate testing, we need to add a method called getChildrenAsJSX:

impl Renderer {
    ...
    pub fn getChildrenAsJSX(&self) -> JsValue {
        let mut children = derive_from_js_value(&self.container, "children");
        if children.is_undefined() {
            children = JsValue::null();
        }
        children = child_to_jsx(children);

        if children.is_null() {
            return JsValue::null();
        }
        if children.is_array() {
            todo!("Fragment")
        }
        return children;
    }
}
Enter fullscreen mode Exit fullscreen mode

This allows us to obtain a tree structure containing JSX objects using the root object. For example, the following code:

const ReactNoop = require('react-noop')
const root = ReactNoop.createRoot()
root.render(
  <div>
    <p>hello</p>
    <span>world</span>
  </div>
)
setTimeout(() => {
  console.log('---------', root.getChildrenAsJSX())
}, 1000)
Enter fullscreen mode Exit fullscreen mode

The final printed result would be:

{
  $$typeof: 'react.element',
  type: 'div',
  key: null,
  ref: null,
  props: {
    children: [
      {
        $$typeof: 'react.element',
        type: 'p',
        key: null,
        ref: null,
        props: {
          children: 'hello',
        },
      },
      {
        $$typeof: 'react.element',
        type: 'span',
        key: null,
        ref: null,
        props: {
          children: 'world',
        },
      },
    ],
  },
}
Enter fullscreen mode Exit fullscreen mode

Note that the code for printing the result is placed inside a setTimeout because we put the update process in a macro task while implementing Batch Update. You can refer to this article for more information.

Next, we include react-noop in the build script and set the build target to nodejs so that we can use it in a Node.js environment. However, to support JSX syntax in Node.js, we need to use Babel. Here, we can directly use babel-node to run our script and configure the necessary presets:

// .babelrc
{
  "presets": [
    [
      "@babel/preset-react",
      {
        "development": "true"
      }
    ]
  ]
}
Enter fullscreen mode Exit fullscreen mode

If everything goes well, the above code should run successfully in Node.js. However, when I tried to use react-noop in Jest, I encountered an error:

work_loop error JsValue(RuntimeError: unreachable
    RuntimeError: unreachable
        at null.<anonymous> (wasm://wasm/00016f66:1:14042)
        ...
Enter fullscreen mode Exit fullscreen mode

Since I couldn't solve the issue, I had to perform unit testing in Node.js instead. Here's an example test case:

async function test1() {
  const arr = []

  function Parent() {
    useEffect(() => {
      return () => {
        arr.push('Unmount parent')
      }
    })
    return <Child />
  }

  function Child() {
    useEffect(() => {
      return () => {
        arr.push('Unmount child')
      }
    })
    return 'Child'
  }

  root.render(<Parent a={1} />)
  await sleep(10)
  if (root.getChildrenAsJSX() !== 'Child') {
    throw new Error('test1 failed')
  }

  root.render(null)
  await sleep(10)
  if (arr.join(',') !== 'Unmount parent,Unmount child') {
    throw new Error('test1 failed')
  }
}
Enter fullscreen mode Exit fullscreen mode

Executing test1 successfully indicates that our React Noop is working correctly.

Please kindly give me a star!!!

💖 💪 🙅 🚩
paradeto
ayou

Posted on June 11, 2024

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

Sign up to receive the latest update from our blog.

Related