Implement React v18 from Scratch Using WASM and Rust - [8] Support Hooks
Tags: react, webassembly, rust
Published: 2024-04-26 07:37:20


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

The previous article implemented support for the `FunctionComponent` type, but it doesn't support Hooks yet. In this article, we'll use `useState` as an example to explain how to implement it.

If you frequently use React, you may have wondered about this: `useState` is imported from the `react` library, but its actual implementation is in `react-reconciler`. How is that achieved? Does React depend on `react-reconciler`?


To understand this issue, let's analyze Big React.


First, let's take a look at the entry file for `useState`:

```ts
// react/index.ts
import currentDispatcher, {
	Dispatcher,
	resolveDispatcher
} from './src/currentDispatcher';

export const useState = <State>(initialState: (() => State) | State) => {
	const dispatcher = resolveDispatcher() as Dispatcher;
	return dispatcher.useState<State>(initialState);
};

export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
	currentDispatcher
};

// react/src/currentDispatcher.ts
...
const currentDispatcher: { current: null | Dispatcher } = {
	current: null
};

export const resolveDispatcher = () => {
	const dispatcher = currentDispatcher.current;

	if (dispatcher === null) {
		console.error('resolve dispatcher时dispatcher不存在');
	}
	return dispatcher;
};

export default currentDispatcher;
```


The code is straightforward. When `useState` is executed, the core logic involves calling the `useState` method on `currentDispatcher.current`. It's evident that `currentDispatcher.current` is initially set to `null`. So, where is it assigned a value? The answer lies in `renderWithHooks`:

```js
// react-reconciler/src/fiberHooks.ts
export const renderWithHooks = (workInProgress: FiberNode) => {
  ...
  currentDispatcher.current = HooksDispatcherOnMount
  ...
}
```

Moreover, the `currentDispatcher` here is not directly imported from `react`, but from the `shared` library. And `shared` ultimately imports `__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED` from `react`, which contains the `currentDispatcher` property:

```js
// react-reconciler/src/fiberHooks.ts
import sharedInternals from 'shared/internals'
const {currentDispatcher} = sharedInternals

// shared/internals.ts
import * as React from 'react'
const internals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
export default internals

// react/index.ts
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
  currentDispatcher,
}
```

So, it forms a dependency relationship like this:

```
react-dom ---depend on--> react-reconciler ---depend on--> shared ---depend on--> react
```

During bundling, `react` and `shared` are bundled together into a `react.js` file. When bundling `react-dom`, `react` needs to be specified as an external dependency. This means that the resulting `react-dom.js` file won't include the code for `react` but will treat it as an external dependency:

```
react + shared => react.js
react-dom + react-reconciler + shared => react-dom.js
```


This approach allows for easy replacement of the renderer. For example, if you want to implement `react-noop` for testing purposes:

```
react-noop + react-reconciler + shared => react-noop.js
```

However, it's apparent that WASM builds don't support externals. So, what can be done? Upon reconsideration, it's realized that to meet the requirements mentioned above, two key points need to be addressed:

- React and renderer code should be bundled separately.
- The renderer should depend on React and be able to modify the values of variables in React at runtime.

We have already achieved the separation of bundling. Now, to implement the second point, which is modifying a variable's value in one WASM module from another WASM module, we refer to the documentation of `wasm-bindgen` and discover that besides exporting functions from WASM for JavaScript usage, it's also possible to import functions from JavaScript for WASM to invoke:

```rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
  fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
  // the alert is from JS
  alert(&format!("Hello, {}!", name));
}
```

So, we can achieve the modification of a variable's value in one WASM module from another by using JavaScript as an intermediary. The specific approach is as follows:

We export an `updateDispatcher` method from `react` to JavaScript, which is used to update `CURRENT_DISPATCHER.current` in `react`.

```rust
fn derive_function_from_js_value(js_value: &JsValue, name: &str) -> Function {
    Reflect::get(js_value, &name.into()).unwrap().dyn_into::<Function>().unwrap()
}

#[wasm_bindgen(js_name = updateDispatcher)]
pub unsafe fn update_dispatcher(args: &JsValue) {
    let use_state = derive_function_from_js_value(args, "use_state");
    CURRENT_DISPATCHER.current = Some(Box::new(Dispatcher::new(use_state)))
}
```

Then, we declare the import of this method in `react-reconciler` (for simplicity, we omitted importing from `shared` here):

```rust
#[wasm_bindgen]
extern "C" {
    fn updateDispatcher(args: &JsValue);
}
```

During `render_with_hooks`, the `updateDispatcher` is called, passing an `Object` that contains the `use_state` property:

```rust

fn update_mount_hooks_to_dispatcher() {
    let object = Object::new();

    let closure = Closure::wrap(Box::new(mount_state) as Box<dyn Fn(&JsValue) -> Vec<JsValue>>);
    let function = closure.as_ref().unchecked_ref::<Function>().clone();
    closure.forget();
    Reflect::set(&object, &"use_state".into(), &function).expect("TODO: panic set use_state");

    updateDispatcher(&object.into());
}
```

Finally, we need to insert a piece of code at the top of the bundled `react-dom/index_bg.js` file to import the `updateDispatcher` method from `react`:

```js
import {updateDispatcher} from 'react'
```

Certainly, this step can be implemented using a script.

To summarize, the above process can be represented simply as:


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


The details of this update can be found [here](https://github.com/ParadeTo/big-react-wasm/pull/4).

Let's test it by modifying the hello-world example:

```ts
import {useState} from 'react'

function App() {
  const [name, setName] = useState(() => 'ayou')
  setTimeout(() => {
    setName('ayouayou')
  }, 1000)
  return (
    <div>
      <Comp>{name}</Comp>
    </div>
  )
}

function Comp({children}) {
  return (
    <span>
      <i>{`Hello world, ${children}`}</i>
    </span>
  )
}

export default App
```

The result is shown below:


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



It looks strange, right? That's because we haven't fully implemented the update process yet.

So far, we have replicated the Big React v3 version. Please kindly give it a star!