ReactのCompositionのレンダリングは遅延評価される
タイトルで既にオチていますが、意外だったので記事にします。
発端
Compositionで実装した
const SomePageTemplate = ({Header, List}) => {
return (
<>
// ...その他機能
<HeaderContainer>
{Header}
<HeaderContainer>
<Container>
{List}
</Container>
</>
)
}
const Page = () => (
<SomePageTemplate
Header={<Header />}
List={<List />}
/>
)
みたいなコンポーネントの一部機能を無効にするために、
const SomePageTemplate = ({Header, List, disableFeature}) => {
return (
<>
// ...その他機能
{!disableFeature && (
<>
<HeaderContainer>
{Header}
<HeaderContainer>
<Container>
{List}
</Container>
</>
)}
</>
)
}
const Page = () => {
const userConfig = useUserConfig()
return (
<SomePageTemplate
Header={<Header />}
List={<List />}
disableFeature={userConfig.disableFeature}
/>
)
}
といったコードが上がってきたので、PageのHeader, Listコンポーネントはpropに入った時点で評価されないのか疑問になったのが事の発端です。
ここではSomePageTemplateに指定されている<Header />および<List />をComposition Componentとし、 {Header}、{List}をComposition位置と呼ぶことにします。
結論
jsxのpropに直接コンポーネントを指定するとReactElementオブジェクトとしてキャッシュされます。その結果、
- Compositionはprop入力時点ではRootからのprops入力しか評価されず、レンダリングが確定するまでComposition Componentの評価はされません。こちらのデモで検証できます。
- ただし、Composition Componentのpropsは即時評価されます。
Composition Componentのレンダリングが遅延評価される証明
import React from 'react';
function LazyLoadingComponent() {
console.log('LazyLoadingComponent initializing')
return <>loaded</>
}
function Composition({Component, disableLoading}) {
if(disableLoading) {
return <>now loading</>
}
return <>{Component}</>
}
export default function App(props) {
return (
<div className='App'>
<h1>Hello React.</h1>
<h2>Start editing to see some magic happen!</h2>
<Composition disableLoading={true} Component={<LazyLoadingComponent/>} />
</div>
);
}
// Log to console
console.log('Hello console')
のコードのdisableLoadingがtrueの時には、LazyLoadingComponent initializingがログに出力されません。falseのときは<>{Component}</>が評価されたタイミングでログ出力されます。
Compositionの親Component内の変数が即時評価される証明
前節の結果からjsx表現はrenderPropsのように関数型のpropとして扱われているように思えますが、propに指定したコンポーネントが単に関数としてwrapされるようではないようです。結論から言うと、Compositionのpropの型はReactElementであり、このReactElementはrenderされるまで、children Nodeを評価しない事によって遅延評価を実現しています。
前節のコードを以上のように書き換えます。
import React from 'react';
function LazyLoadingComponent({text}) {
console.log('LazyLoadingComponent initializing')
return <>loaded {text}</>
}
function Composition({Component, disableLoading}) {
if(disableLoading) {
return <>now loading</>
}
return <>{Component}</>
}
export default function App(props) {
const hello = () => {
console.log('hello')
return 'hello'
}
return (
<div className='App'>
<h1>Hello React.</h1>
<h2>Start editing to see some magic happen!</h2>
<Composition disableLoading={true} Component={<>
<LazyLoadingComponent text={hello()} />
</>} />
</div>
);
}
// Log to console
console.log('Hello console')
このようにすると、レンダリング結果にはnow loadingが表示されますが、consoleにはhelloの文字列も出力されます。
つまり、Compositionに指定したComponentはレンダリングが確定するまで評価されませんが、Compositionのprop内のscopeで利用している変数に関しては、親コンポーネントのスコープ内で即時評価されています。
Compositionコンポーネント内でconsole.log(Component)を実行するとReactElement型の値が出力されることも確認出来ます。
以上からComposition propは指定された時点ではコンポーネントのレンダリングを開始しませんが、propに指定されたjsx表現の評価結果をReactElement型のオブジェクトとしてキャッシュし、レンダリング開始時に評価する事が結論づけられます。
ReactElementの詳細についてはこちらの記事が詳しいのでご参考にされるとよいかと思います。
以上から、Composition内の変数の初期化にも処理時間が発生することを防ぎたい場合は以下のように条件文でComposition propに対する評価を抑制する必要があります。
export default function App(props) {
const hello = () => {
console.log('hello');
return 'hello';
};
const disableLoading = true
return (
<div className="App">
<h1>Hello React.</h1>
<h2>Start editing to see some magic happen!</h2>
<Composition
disableLoading={disableLoading}
Component={!disableLoading &&
<>
<LazyLoadingComponent text={hello()} />
</>
}
/>
</div>
);
}
以上の例では、条件処理によってconsole.log('hello');は実行されないようにしています。
まとめ
- Compostionコンポーネントのレンダリングは遅延評価されます
- Compostionコンポーネントのpropsは指定時に即時評価されます。
- 以上を踏まえて、不要なレンダリングのみを抑制したい場合はComposition位置のレンダリングを抑制するだけでも十分ですが、変数の初期化処理にも時間がかかる場合は条件文や、変数の初期化自体を抑制し、prop指定時点で評価されないようにする必要があります。
Discussion