この投稿はReact 18を前提にしています。 また、関数コンポーネントのことを指してコンポーネントと書いています。
Reactでは、様々なコンポーネントを実装して組み合わせることによってアプリケーションを作っていきます。 時にはJSXを変数に代入したり、JSXを返すrender関数を作ることもあります。 Reactは宣言的に書けて楽しくて便利ですよね。僕は好きです。
では、JSXを返すrender関数とコンポーネントは何が違うのでしょうか。
普段の開発では意識することはないかもしれませんが、
僕はrender hooksパターンを実装するときに疑問に思いました。
どちらもJSXを返す関数ですが、renderComponent()
や、<Component />
のように異なった呼び出され方をします。
記事では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関数とコンポーネントでどのように挙動が異なっているのかを見ていきます。 実行できるコードを提示した後、なぜそのような挙動になっているのかを説明していきます。
まず、初回レンダリングが遅いコンポーネントを用意します。 初回レンダリングでは、再レンダリング時に行えるメモ化などの最適化ができないため、比較的重たいです。 また、コンポーネントのマウント時には、追加で様々な処理が行われるため、さらに時間がかかることもあります。 以降は「初回レンダリング+マウント」をまとめてマウントと呼ぶことにします。
tsxconst SlowMount = () => { const firstRender = useRef(true); if (firstRender.current) { [...new Array(10000)].forEach(() => { console.log("delay"); }); firstRender.current = false; } return <div />; };
このコンポーネントを使用したとき、render関数とコンポーネントの挙動に違いがでてきます。 挙動の確認には以下のようなコードを使用します。
tsxexport 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
コンポーネントのレンダリングのたびに異なる関数として作成され、別のコンポーネントとして扱われるからです。
別のコンポーネントとして扱われているので、アンマウントとマウントが発生します。
具体的には以下のような流れで処理されます。
setCounter
が呼び出されるとApp
コンポーネントが再レンダリングされるCounter
関数が作成されるCounter
関数と今回のCounter
関数を別のコンポーネントとして認識するCounter
がアンマウントされ、新しいCounter
がマウントされるこのように、Counter
関数が毎回新しく生成されることで、レンダリングのたびにマウントが発生します。
一見するとCounter
は同じコンポーネントなのですが、Reactは別のコンポーネントとして扱います。
ReactではuseCallback
を使って関数をメモ化することもできますが、今回のケースだと依存リストにcounter
を含める必要があるので、
counter
を変更すると結局マウントが発生することになります。
また、SlowMount
にReact.memo
を使用しても、親のCounter
がアンマウントされるので意味がないですし、
Counter
関数にReact.memo
を使用しても、何度もReact.memo
が呼ばれるだけなので異なる関数が返ってきます。
また、コンポーネントが状態を持っている場合、アンマウントとマウントによってリセットされるという問題もあり、バグに繋がる可能性があります。
ここまででレンダリングのたびに異なるCounter
関数が作成されるため、Reactが別のコンポーネントだと認識してしまい、
アンマウントとマウントが発生することはわかったと思います。
しかし、Counter
関数がレンダリングのたびに異なっているにもかかわらず、render関数ではSlowMount
がアンマウントされません。
render関数でSlowMount
がアンマウントされないのは、アンマウントやマウントがコンポーネントに固有のものだからです。
SlowMount
がアンマウントされたのは、親であるCounter
コンポーネントがアンマウントされたからで、render関数としてのCounter
にはマウントもアンマウントもありません。
ReactはJSXの<Counter />
という記法やcreateElement
関数によってコンポーネントからReact要素を生成し、最終的に画面に描画します。
コンポーネント以外でも、div
やp
などのHTMLタグからReact要素を生成し、画面に描画することもできます。
JSXである<Component />
というコードは、TypeScriptやBabelといったツールによって_jsx(Component, ...)
というコードに変換されます。
React 17以前では上にあるcreateElement
が使われていましたが、パフォーマンス改善や簡略化のために_jsx
が使われるようになりました。
Counter()
と呼び出す場合、Counter
関数はコンポーネントではないので、Counter
関数内部にあるJSXがReact要素として生成されます。
SlowMount
はトップレベルで作られた関数なのでレンダリング間で同一であり、App
コンポーネントが再レンダリングされてもアンマウントされません。
JSXを返す関数について、
render()
のように呼ばれるものをrender関数<Component />
のように呼ばれるものをコンポーネントとした場合、マウント・アンマウントはコンポーネントに固有の概念であり、