Wasm を扱う Custom Hooks を書く
概要
React で Wasm を扱う簡単な Custom Hooks を書く. なお, Wasm は Rust コードを wasmbuild でビルドしたものを使用する.
環境
- React 18.1.0
- Deno 1.38.5
- wasmbuild 0.15.4
- rustc 1.70.0
Custom Hooks の概要
wasmbuild は <crate-name>_bg.wasm
とそのバインディングである <crate-name>.generated.js
を生成する. クレート名が wasm
のとき, これらは以下のようにして使用できる.
import { instantiate } from 'wasm.generated.js';
const exports = await instantiate({
url: new URL('wasm_bg.wasm', location.origin),
});
const { add } = exports;
console.log(add(1, 2)); // 3
この処理を Context で隠蔽し, Custom Hooks として使用できるようにする.
Custom Hooks の実装
Context Provider として WasmProvider
を, Custom Hooks として useWasm
を実装する. Context の型は, wasm ファイルの読み込み状況とその状況に応じてエクスポートされたオブジェクトまたはエラーを持つ WasmState
とする. また, WasmProvider
は src
として wasm ファイルの URL を受け取るようにする.
import { instantiate } from 'wasm.generated.js';
const WasmLoadState = {
Ready: 'ready',
Loading: 'loading',
Error: 'error',
} as const;
type WasmLoadState = (typeof WasmLoadState)[keyof typeof WasmLoadState];
type WasmExports = Awaited<ReturnType<typeof instantiate>>;
type WasmState = {
loadState: typeof WasmLoadState.Loading;
} | {
loadState: typeof WasmLoadState.Error;
error: Error;
} | {
loadState: typeof WasmLoadState.Ready;
exports: WasmExports;
};
type WasmProviderProps = {
src: string;
children: ReactNode;
};
const WasmProvider: FC<WasmProviderProps> = ({ src, children }) => {
...
};
instantiate
がエクスポートされたオブジェクトを返す非同期関数であるため, 上記のように Awaited
と ReturnType
で WasmExports
を定義できる.
WasmProvider
は instantiate
の状況に合わせて WasmState
を更新し, それを WasmContext
に渡せばよいので, 以下のように実装できる.
const initialWasmState: WasmState = {
loadState: WasmLoadState.Loading,
};
const WasmContext = createContext<WasmState>(initialWasmState);
const WasmProvider: FC<WasmProviderProps> = ({ src, children }) => {
const [wasmState, setWasmState] = useState<WasmState>(initialWasmState);
useEffect(() => {
(async () => {
try {
const exports = await instantiate({
url: new URL(src),
});
setWasmState({
loadState: WasmLoadState.Ready,
exports,
});
} catch (e) {
setWasmState({
loadState: WasmLoadState.Error,
error: e,
});
}
})();
}, [src]);
return (
<WasmContext.Provider value={wasmState}>
{children}
</WasmContext.Provider>
);
};
useWasm
は WasmContext
を使用して WasmState
を取得するだけである.
const useWasm = () => useContext(WasmContext);
全ての実装は以下のようになる. (長いので畳んでおく)
useWasm.tsx
import React, {
createContext,
FC,
ReactNode,
useContext,
useEffect,
useState,
} from 'react';
import { instantiate } from 'wasm';
const WasmLoadState = {
Ready: 'ready',
Loading: 'loading',
Error: 'error',
} as const;
type WasmLoadState = (typeof WasmLoadState)[keyof typeof WasmLoadState];
type WasmProviderProps = {
src: string;
children: ReactNode;
};
type WasmExports = Awaited<ReturnType<typeof instantiate>>;
type WasmState = {
loadState: typeof WasmLoadState.Loading;
} | {
loadState: typeof WasmLoadState.Error;
error: Error;
} | {
loadState: typeof WasmLoadState.Ready;
exports: WasmExports;
};
const initialWasmState: WasmState = {
loadState: WasmLoadState.Loading,
};
const WasmContext = createContext<WasmState>(initialWasmState);
const WasmProvider: FC<WasmProviderProps> = ({ src, children }) => {
const [wasmState, setWasmState] = useState<WasmState>(initialWasmState);
useEffect(() => {
(async () => {
try {
const exports = await instantiate({
url: new URL(src),
});
setWasmState({
loadState: WasmLoadState.Ready,
exports,
});
} catch (e) {
setWasmState({
loadState: WasmLoadState.Error,
error: e,
});
}
})();
}, [src]);
return (
<WasmContext.Provider value={wasmState}>
{children}
</WasmContext.Provider>
);
};
const useWasm = () => useContext(WasmContext);
export { WasmLoadState, WasmProvider };
export default useWasm;
上記の実装のメリットとして
- Context を使用しているため, 複数コンポーネントに渡って Wasm を使用する場合でも一度のインスタンス化で済む.
- wasmbuild のグルーコードによる型付けの恩恵を受けられる.
ことが挙げられる.
使用例
以下のようにして使う.
import React from 'react';
import ComponentWithWasm from './ComponentWithWasm';
import { WasmProvider } from './useWasm';
const App = () => {
return (
<WasmProvider src='wasm_bg.wasm'>
<ComponentWithWasm />
</WasmProvider>
);
};
export default App;
import React from 'react';
import useWasm, { WasmLoadState } from './useWasm';
const ComponentWithWasm = () => {
const wasm = useWasm();
return (
<div>
{wasm.loadState === WasmLoadState.Loading && (
<p>Loading...</p>
)}
{wasm.loadState === WasmLoadState.Error && (
<p>Error: {wasm.error.message}</p>
)}
{wasm.loadState === WasmLoadState.Ready && (
<p>1 + 2 = {wasm.exports.add(1, 2)}</p>
)}
</div>
);
};
export default ComponentWithWasm;
最後に
wasmbuild の生成物を使用する Custom Hooks が見つからなかったため作成した. ローディングメッセージとエラーメッセージだけ表示すれば, 他は通常の関数と同様に扱えるため, 非常に便利である.
Discussion