見出し画像

スタンダードビューとReact-3-Fiber コンテキストブリッジの紹介

はじめに
スタンダードは、最新の人工知能技術により、実店舗の小売業者向けにレジ無し決済システムを可能にします。私たちの機械学習アルゴリズムが、店舗内で起こることをどのように理解しているかをよりよく可視化するために、私たちはStandard Viewを構築しました。Standard View の目的は、グラフィックエンジニアではない方々のために three.js の複雑さを隠して、React を使った 3D ツール開発を加速させることです。このライブラリは、react-three-fiberの上に構築されており、Reactのシンタクスを使って宣言的にthree.jsのコンポーネントを定義することができるライブラリです。
Standard Viewは、私たちの多くの内部ツールの基礎となるブロックです。これらのライブラリを組み合わせる際、ツールの開発中に厄介なバグに遭遇しました。その一つが react-redux です。react-three-fiberを他の状態管理ライブラリやReactコンテキストと組み合わせて使用すると、同じ問題に遭遇する可能性があります。この記事では、私たちがどのように問題を解決し、解決策を一般化し、Reactの内部を探ったかを説明します。
 
問題点
 
react-three-fiber で 3D コンポーネントを描画するには、すべてのコンポーネントを react-three-fiber の Canvas コンポーネント内にラップする必要があります。このラッパー・コンポーネントは、react-three-fiber のリコンサイラーと React のデフォルトのリコンサイラーを分離するための境界として機能します。リコンサイラーは、JSXを別のフォーマットに変換するライブラリです。デフォルトでは、JSXをDOMに変換します。React-three-fiberは、three.jsのノードを生成するためにこれを使用します。
子コンポーネントは、ラッパー内のコンテキストデータを作成したり読み取ったりすることができます。フックも同様に、このデータにアクセスすることができます。このラッパーの外側にある一般的なコンポーネントも、これを行うことができます。しかし、コンテキスト・データはラッパーを越えては流れません。これはreact-three-fiberのGithubページで既知の制限事項です。
 
コンテキストの境界線は、私たちにとって問題です。私たちは、ビジュアライゼーションのオン/オフを切り替えることができるUIを構築したかったのです。しかし、この制限により、アプリケーションをCanvasコンポーネントの内部に包むことが提案されています。私たちは、むしろトグルコントロールと3Dコンポーネントを分離し、2つの間で何らかのコンテキストを共有したいと考えています。そこで、私たちは、コンテキストのブリッジを構築することによって、この2つを接続することを試みました。

シンプルなコンテキストブリッジ
最も単純なコンテキストブリッジは、外側のコンテキストコンシューマーと内側のコンテキストプロバイダを手動で接続します*。
<Context.Consumer>
{value => (
<Canvas>
<Context.Provider value={value}>
<Square />
</Context.Provider>
</Canvas>
)}
</Context.Consumer>
キー・アイデア:

  • リファレンス値が、外部からのハードコーディングされます。<Context.Consumer />, は、Canvas コンポーネント全体に渡って<Context.Provider />

  • <Context.Consumer /> は、その値が変更されたときにのみ更新をトリガーします。

  • <Context.Provider /> は、すべての新しい値に対して浅い比較を行い、再レンダリングが必要かどうかを判断します。この最適化により、Reactが不必要な再レンダリングするのを防ぐことができます。

一般的なコンテキストブリッジ
素晴らしい! シンプルなContextBridgeは、正確に1つのコンテキストを共有して動作します。一般的には、もっともっと多くのコンテキストを扱わなければならない。そこで、一般的な解決策を考えてみましょう。上記の解決策は、コンテキストリスナー、コンテキストプロバイダ、ブリッジユニットの3つの要素に分解されます。コンテキスト・リスナーは ContextListeners コンポーネントに一般化でき、外部コンテキストからのすべての変更をリスニングします。コンテキストプロバイダは、ContextProvidersコンポーネントで、更新されたすべてのコンテキストを渡す。そして、ブリッジユニットであるCanvasWithContextBridgeコンポーネントは、2つのコンポーネントを接続し、Canvasコンポーネントを置き換えます。

ContextListeners
ContextListenersコンポーネントは、外側のcontextからの変更をリッスンします。そのためには、contextのリストと各Contextの値のリストにアクセスする必要があります。そして、それぞれについて、それぞれの値を共有リストにキャッシュします。
const ContextListeners = memo(({
contexts,
values
}) => contexts.map(
(context, index) => (
<ContextListener
context={context}
values={values}
index={index}
/>
)
))
 
const ContextListener = memo(({ context, values, index }) => {
values[index] = useContext(context);
return null;
})
キー・アイデア:

  • useContext フックを使って、最新のコンテキスト値を自動的に取得する。

  • コンテキストを通してループさせたいのですが、これはフックのルールに違反します。map関数を使うことでチェックを回避することができます。

注:リスナーは、単純なContextBridgeのようにプロバイダをラップしなくなったので、子コンポーネントのレンダリングは削除されました。
ContextProviders コンポーネント
ContextProvidersコンポーネントは、Canvasコンポーネント内のすべての子コンポーネントにコンテキスト値の更新をプッシュします。これは、ContextListenersとCanvasの子から、コンテキストのリストと値のリストを受け取ります。
const ContextProviders = memo(({
contexts,
values,
children
}) => {
const [, update] = useState();
 
useFrame(() => update({}));
 
if (!contexts || !values) {
return <>{children}</>;
}
 
return contexts.reduce(
(child, Context, index) => (
<Context.Provider value={values[index]}>
{child}
</Context.Provider>
),
children
);
});
キー・アイデア:

  • ここでもまた、フックのルールを回避する必要があります。reduce 関数を使用して、<Context.Provider /> コンポーネントを再帰的に埋め込みます。

  • React にコンテキストの値を再チェックするように指示する方法が必要です。react-three-fiber が提供する useFrame フックを使用して、次のレンダー呼び出しが実行される前に、このチェックをキューに入れます。

  • 空の更新セッターは、コンポーネントの再レンダリングが必要かどうかをReactに再確認させるために使用されます。

注意: このフレームごとのチェックは、実際には高価ではありません。更新がアニメーションフレームより速く行われた場合、これらはバッチ処理され、次のフレームで一度だけレンダリングされます。一方、更新がまばらな場合、Context.Provider の浅い比較チェックにより、不必要な再レンダリングが防止されます。値が変更され、子コンポーネントがそのコンテキストを使用するときだけ、レンダリングがトリガされます。
 
CanvasWithContextBridgeコンポーネント
CanvasWithContextBridgeは、react-three-fiberのCanvasコンポーネントと上記のContextListeners、ContextProviderを接続します。
const CanvasWithContextBridge = memo(({contexts, children}) => {
const values = useRef([])
return (
<>
<ContextListeners contexts={contexts} values={values.current} />
<Canvas>
<ContextProviders contexts={contexts} values={values.current}>
{children}
</ContextProviders>
</Canvas>
</>
)
})
キー・アイデア:

  • コンテキストの値の共有配列にuseRefフックを使います。

With this last part, the CanvasWithContextBridge is ready to replace existing uses of react-three-fiber's Canvas. 🥳
問題の解決
コンテキスト境界の問題は、以下のコードで問題なくなります。この解決策は、コンテキスト値がtrueのときに2つの空のメッシュをレンダリングします。この例の作業コピーはこちらでご覧になれます。
import React, { createContext } from "react";
 
const Context = createContext();
 
const Toggle = () => {
const toggle = useContext(Context);
return <input type="checkbox" checked={toggle} />;
}
 
const ToggleableMesh = () => {
const toggle = useContext(Context);
return toggle ? <mesh> : null;
}
 
const Page = () =>
<CanvasWithContextBridge contexts={[Context]}>
<ToggleableMesh />
<mesh />
</CanvasWithContextBridge>
 
 
const App = () =>
<Context.Provider value={true}>
<Toggle />
<Page />
</Context.Provider>
結論
このコンテキストブリッジは、Standard Cognitionの内部で使用しているものです。Standard Viewの中のView3Dを動かしています。Reactとreact-three-fiberの仕組みについて、ご理解いただけたでしょうか。また、このソリューションによって、react-three-fiberを使用してさらに複雑で素晴らしいアプリケーションを構築できるようになることを願っています。簡単なコンテキストブリッジの動作例と完全なコンテキストブリッジは、こちらでご覧いただけます。Standard Viewは、オープンソース化されており、こちらで入手可能です。
 
追記:カスタムコンテキスト以外にも、私たちの3Dツールは主にreduxとreact-reduxで動作します。以下の行をアプリケーションに追加することで、redux の状態を react-three-fiber canvas (es) に取り込むことができます。
import { ReactReduxContext } from "react-redux";
 
<CanvasWithContextBridge contexts={[ReactReduxContext]} />
* Solution provided by francisco
1.    https://github.com/facebook/react/issues/13332#issuecomment-513088081
この記事は2020年8月24日にメイチー・チナバニチキット(Maytee Chinavanichkit)によって書かれた記事です。