🪝

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 のグルーコードによる型付けの恩恵を受けられる.

ことが挙げられる.

使用例

以下のようにして使う.

App.tsx
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;
ComponentWithWasm.tsx
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 が見つからなかったため作成した. ローディングメッセージとエラーメッセージだけ表示すれば, 他は通常の関数と同様に扱えるため, 非常に便利である.

参考文献

GitHubで編集を提案

Discussion