avatar
2024年 11月 16日

Reactのrender関数とコンポーネントの違い

この投稿はReact 18を前提にしています。 また、関数コンポーネントのことを指してコンポーネントと書いています。

Reactでは、様々なコンポーネントを実装して組み合わせることによってアプリケーションを作っていきます。 時にはJSXを変数に代入したり、JSXを返すrender関数を作ることもあります。 Reactは宣言的に書けて楽しくて便利ですよね。僕は好きです。

では、JSXを返すrender関数とコンポーネントは何が違うのでしょうか。 普段の開発では意識することはないかもしれませんが、 僕はrender hooksパターンを実装するときに疑問に思いました。 どちらもJSXを返す関数ですが、renderComponent()や、<Component />のように異なった呼び出され方をします。 記事ではrender関数を使っていますが、コンポーネントではだめなのでしょうか。

この投稿では、これらの違いについて見ていきます。

render関数・コンポーネントの定義

まず、用語が何を指すのかを簡単に定義します。

どちらにも共通して言えるのは、「JSXを返す関数」ということです。どちらもただの関数で、好きにネストしたり代入できます。 コンポーネントはコンポーネント内にネストすることは推奨されていませんが、 後述の説明によって理由は理解できると思います。

tsx
// render関数 const render = () => { // render関数 const renderB = () => { return <div>Render B</div>; }; return <div>Render{renderB()}</div>; }; // コンポーネント const Component = () => { return <div>Component</div>; }; // コンポーネント const ComponentB = Component; const App = () => { return ( <> {render()} <Component /> <ComponentB /> </> ); };

render関数とコンポーネントはただの関数ですが、この投稿では呼び出し方によって区別します。 render()のように呼び出された関数をrender関数、JSXで<Component />のように呼び出された関数をコンポーネントと呼びます。

上の<Component />はrender関数のようにComponent()と呼び出すこともできるのですが、 コンポーネントは先頭が大文字、またはドット記法を使う必要があり、それ以外の場合には組み込みのHTMLタグとして扱われることになります。 そのため、<render />のように書いてしまうと存在しないHTMLタグが使われているといったエラーが発生します。 ドット記法が使われている場合には先頭を大文字にする必要はなく、例えばmotionというアニメーションのためのライブラリでは、 <motion.div />のようなコンポーネントが使われます。

それぞれの挙動の違い

次はrender関数とコンポーネントでどのように挙動が異なっているのかを見ていきます。 実行できるコードを提示した後、なぜそのような挙動になっているのかを説明していきます。

検証のために使用するコード

まず、初回レンダリングが遅いコンポーネントを用意します。 初回レンダリングでは、再レンダリング時に行えるメモ化などの最適化ができないため、比較的重たいです。 また、コンポーネントのマウント時には、追加で様々な処理が行われるため、さらに時間がかかることもあります。 以降は「初回レンダリング+マウント」をまとめてマウントと呼ぶことにします。

tsx
const SlowMount = () => { const firstRender = useRef(true); if (firstRender.current) { [...new Array(10000)].forEach(() => { console.log("delay"); }); firstRender.current = false; } return <div />; };

このコンポーネントを使用したとき、render関数とコンポーネントの挙動に違いがでてきます。 挙動の確認には以下のようなコードを使用します。

tsx
export default function App() { const [isRender, setIsRender] = useState(true); const [counter, setCouter] = useState(0); const Counter = () => { return ( <div> <SlowMount /> <button onClick={() => { setCouter((c) => c + 1); }} > + </button> <div>{counter}</div> </div> ); }; return ( <div> <div> {isRender ? "render関数" : "コンポーネント"} <button onClick={() => { setIsRender((r) => !r); }} > switch </button> </div> {isRender ? Counter() : <Counter />} </div> ); }

このコードは、SlowMountコンポーネントを内部で使用しているCounterを表示するものです。 Counter関数はisRenderという状態を変更することによって、render関数とコンポーネントを切り替えることができます。 Appコンポーネントは内部にカウンターの状態を持っており、Counter関数はその状態を表示・更新します。

コンポーネントで発生する遅延

Appコンポーネントをレンダリングして何度か+ボタンを押すと、render関数と比べてコンポーネントはカウントが表示されるまでに遅延があります。 +ボタンを押すたびにconsole.logが表示されていることから、再レンダリングではなく、アンマウントされてマウントが実行されていることがわかります。

この原因は、Counter関数がAppコンポーネントのレンダリングのたびに異なる関数として作成され、別のコンポーネントとして扱われるからです。 別のコンポーネントとして扱われているので、アンマウントとマウントが発生します。 具体的には以下のような流れで処理されます。

  1. setCounterが呼び出されるとAppコンポーネントが再レンダリングされる
  2. 再レンダリング時に新しいCounter関数が作成される
  3. Reactは前回のレンダリングで作成されたCounter関数と今回のCounter関数を別のコンポーネントとして認識する
  4. 前回のCounterがアンマウントされ、新しいCounterがマウントされる

このように、Counter関数が毎回新しく生成されることで、レンダリングのたびにマウントが発生します。 一見するとCounterは同じコンポーネントなのですが、Reactは別のコンポーネントとして扱います。

ReactではuseCallbackを使って関数をメモ化することもできますが、今回のケースだと依存リストにcounterを含める必要があるので、 counterを変更すると結局マウントが発生することになります。 また、SlowMountReact.memoを使用しても、親のCounterがアンマウントされるので意味がないですし、 Counter関数にReact.memoを使用しても、何度もReact.memoが呼ばれるだけなので異なる関数が返ってきます。

また、コンポーネントが状態を持っている場合、アンマウントとマウントによってリセットされるという問題もあり、バグに繋がる可能性があります。

render関数で遅延が発生しない理由

ここまででレンダリングのたびに異なるCounter関数が作成されるため、Reactが別のコンポーネントだと認識してしまい、 アンマウントとマウントが発生することはわかったと思います。 しかし、Counter関数がレンダリングのたびに異なっているにもかかわらず、render関数ではSlowMountがアンマウントされません。

render関数でSlowMountがアンマウントされないのは、アンマウントやマウントがコンポーネントに固有のものだからです。 SlowMountがアンマウントされたのは、親であるCounterコンポーネントがアンマウントされたからで、render関数としてのCounterにはマウントもアンマウントもありません。

ReactはJSXの<Counter />という記法やcreateElement関数によってコンポーネントからReact要素を生成し、最終的に画面に描画します。 コンポーネント以外でも、divpなどのHTMLタグからReact要素を生成し、画面に描画することもできます。

JSXである<Component />というコードは、TypeScriptやBabelといったツールによって_jsx(Component, ...)というコードに変換されます。 React 17以前では上にあるcreateElementが使われていましたが、パフォーマンス改善や簡略化のために_jsxが使われるようになりました。

Counter()と呼び出す場合、Counter関数はコンポーネントではないので、Counter関数内部にあるJSXがReact要素として生成されます。 SlowMountはトップレベルで作られた関数なのでレンダリング間で同一であり、Appコンポーネントが再レンダリングされてもアンマウントされません。

まとめ

JSXを返す関数について、

  • render()のように呼ばれるものをrender関数
  • <Component />のように呼ばれるものをコンポーネント

とした場合、マウント・アンマウントはコンポーネントに固有の概念であり、

  • レンダリング間でコンポーネントが同一ではない場合、アンマウントされる
  • render関数はコンポーネントではないため、アンマウントされない