「propsで違う値を渡しているはずなのにコンポーネントに更新が走らない」
「Reactのレンダリングがイマイチ理解できていない状態で開発している」
「mapでkeyを書き忘れると警告文が出ることがあるが、何のためにkeyが必要かがわからない」
本日はそんな方に向けてReactのレンダリングを理解してもらう内容になります。
Reactでコンポーネントがレンダリングされないという問題は初学者にとっては何回も遭遇することがあります。
この記事では、Reactアプリケーションでコンポーネントが正しく表示されない1例を紹介します。
またReactでコンポーネントがレンダリングされない場合、keyプロパティの使用方法を理解することで解決できるのでkeyの使い方も一緒に解説します。
動画でも解説しているので必要な方はご覧ください。
Reactのレンダリングとは?
そもそもレンダリングというのはDOMの更新と言ったりするのですが、初心者の方でイメージしづらければ「画面が更新される」といった理解でも問題ないでしょう。
ただしレンダリングにはブラウザの画面更新と違って何点かルールがあるので注意が必要です。
親コンポーネントがレンダリングされたら子コンポーネントもレンダリングされる
まずは親子関係になっている構成において親がレンダリングされたら子は何の変更が無かったとしてもレンダリングされるという点です。
以下の画面ではTitle.jsとChild.jsとNewDate.jsという3つの子コンポーネントを読み込んでいる状態です。
import React, { useState } from 'react';
import Child from './Child';
import Title from './Title';
import NewDate from './NewDate';
const App = () => {
console.count('レンダリング');
const [count, setCount] = useState(0);
return (
<div>
<Title />
<button onClick={() => setCount((prev) => prev + 1)}>+</button>
<Child count={count} />
<button onClick={() => setCount((prev) => prev - 1)}>-</button>
<NewDate />
</div>
);
};
export default App;
import React from 'react';
const Title = () => {
console.count('タイトルがレンダリング');
return <h1>カウンター</h1>;
};
export default Title;
import React from 'react';
const Child = ({ count }) => {
console.count('子コンポーネントがレンダリング');
return <span>{count}</span>;
};
export default Child;
import React, { useState } from 'react';
const NewDate = () => {
const [date, setDate] = useState('');
console.count('日時が更新されました');
const newDate = () => {
setDate(new Date().toString());
};
return (
<div>
{date}
<button onClick={() => newDate()}>日時表示</button>
</div>
);
};
export default NewDate;
それぞれのコンポーネントにはレンダリングされたときにconsole.logが走るようにしておきました。
それでは親であるApp.jsでブラウザのリロードをかけてみると以下のようになります。
子コンポーネントについても何も変更がなくても親と一緒にレンダリングされていることがわかります。
一方で子コンポーネントだけに変更があったときはどうなるでしょうか。
実は変更があった子コンポーネントのみがレンダリングされて親コンポーネントはレンダリングされません。
少し不思議な現象に見えるかもしれませんが、親コンポーネントというのは画面リロードなど自分自身に変更がある時のみレンダリングされます。
子コンポーネントは自分自身の変更はもちろん、親コンポーネントで何かしら変更や画面リロードがあったときにもレンダリングされる仕組みです。
Reactでpropsで違う値を渡しているはずなのにコンポーネントに更新が走らない?
レンダリングの基本を抑えた上でよくあるミスを共有してみたいと思います。
まずは例題として以下のようなカウンターアプリを想定します。
import "./App.css";
import Render from "./screens/wrongIfRender/Render";
function App() {
return (
<div className="App">
<Render />
</div>
);
}
export default App;
import React from "react";
import { useState } from "react";
import Counter from "../wrongIfRender/Counter";
const Render = () => {
const [isAdmin, setAdmin] = useState(true);
return (
<div>
{isAdmin ? <Counter name="管理者" /> : <Counter name="スタッフ" />}
<br />
<button onClick={() => setAdmin((user) => !user)}>切り替え</button>
</div>
);
};
export default Render;
import React, { useState } from "react";
const Counter = ({ name }) => {
const [count, setCount] = useState(0);
return (
<div>
<h1>{name}</h1>
<button onClick={() => setCount((current) => current + 1)}>+</button>
<button onClick={() => setCount((current) => current - 1)}>-</button>
<p>現在の数字は{count}です</p>
</div>
);
};
export default Counter;
Reactを学習されている方なら1度は練習したことのあるシンプルなカウンターですね。
少し手を加えているのがRender.jsにてisAdminというステートを作っている点です。
こちらはtrue,falseの値を取って、管理者かスタッフかというユーザーの判定を行っていると思ってください。
「切り替え」というボタンをクリックすることでisAdminの値がtrueとfalseに交互に切り替わることで管理者とスタッフの表示が切り替わる形です。
例えば管理者の状態でボタンをクリックするとスタッフに切り替わり、またその逆もできます。
こちらのアプリですが動作させること自体はそこまで難しくないのですが1点良くない点があります。
カウントした結果が管理者とスタッフを切り替えても残ってしまう点です。
このような管理者とスタッフを切り替えるアプリではそれぞれ別で動作させることが多く、管理者とスタッフを切り替えるとカウントも1度リセットさせたいことがあるわけです。
カウントがリセットされないということは子コンポーネントであるCounter.jsではレンダリング(更新)が行われていないことを意味しています。
Reactを勉強し始めた時に「レンダリング」という言葉を何回も見聞きしたことでしょう。
しかし実際のところレンダリングを意識する意味がわかっていない方は多いはずです。
上記の例はレンダリングによって困るパターンあるあるですので一度は経験しておくと良いでしょう。
一方で少し理解されている方であれば逆に疑問に思うかもしれません。
「変更があった時にレンダリングされるはずなのに、ユーザーを切り替えてもレンダリングされないのはなぜ?」
とても良いところに目を付けれています。
おっしゃる通りです、レンダリングは画面をリロードせずとも子コンポーネントに変更があれば子コンポーネントだけはレンダリングが実行されます。
しかしよく考えてみて欲しいのですが、ユーザーの切り替えはCounter.jsから見た親コンポーネントであるRender.jsで監視していましたね。
そのためユーザーの切り替えだけではCounter.jsは「変更なし」と判断されてレンダリングされないわけです。
Reactのkeyは何のために書くのか?
それではどうするか?というお話です。
基本的には子コンポーネントに変更があればレンダリングされることには変わりないです。
しかし今回の場合は先ほどお話したようにユーザーは親コンポーネントであるRender.jsで管理しているため、ユーザーの切り替え以外で子コンポーネントであるCounter.jsに変更を加えねばなりません。
一番手取り早いのは以下のように表示する内容を変えることです。
import React from "react";
import { useState } from "react";
import Counter from "../wrongIfRender/Counter";
const Render = () => {
const [isAdmin, setAdmin] = useState(true);
return (
<div>
{/* ここを変更 */}
{isAdmin ? (
<div>
<Counter name="管理者" />
</div>
) : (
<section>
<Counter name="スタッフ" />
</section>
)}
<br />
<button onClick={() => setAdmin((user) => !user)}>切り替え</button>
</div>
);
};
export default Render;
Counter.jsを管理者、スタッフそれぞれで別のHTMLタグで囲んでみました。
画面上は特に変わりないですが、ユーザーによって違う表示をさせることになりますよね。
これで期待したような動作にすることができました。
まずはこのやり方を覚えておきましょう、レンダリングの有無がイメージしやすくなるはずです。
しかし実務では使いにくい方法と言われがちです。
そこでmapメソッドでも登場したkeyを使っても同じことができます。
import React from "react";
import { useState } from "react";
import Counter from "../wrongIfRender/Counter";
const Render = () => {
const [isAdmin, setAdmin] = useState(true);
return (
<div>
{/* ここを変更 */}
{isAdmin ? (
<Counter name="管理者" key="admin" />
) : (
<Counter name="スタッフ" key="staff" />
)}
<br />
<button onClick={() => setAdmin((user) => !user)}>切り替え</button>
</div>
);
};
export default Render;
全く同じ動作になっています。
mapメソッドを使うときにkeyを書いていないと警告文やコンソールエラーが表示された経験がある方は多いと思います。
学習教材やスクールでは「mapにはkeyが必要だから」くらいにしか解説されませんが、実は重要な役割があります。
コンポーネントにkeyで文字列などの値を書いておくことで、それが目印のような役割になって繰り返しの順番やコンポーネントの変化を読み取ってくれるようになります。
詳しくは公式ドキュメントを確認してみてください↓
https://ja.legacy.reactjs.org/docs/lists-and-keys.html#keys
今回の例題は「レンダリングさせたい」というケースでお話していますが、もし「レンダリングさせたくない」というケースではkeyを付けないようにしてくださいね。
「レンダリングの善悪」ではなく、それぞれの違いと対応方法をしっかり把握しておくことが一番大事です。