こんにちは、とまだです。
はじめに
この記事の内容
React を使っている方のほとんどが、「コンポーネントを作る」という経験をしていると思います。
しかし、コンポーネントを作るだけでなく、コンポーネントを「改善する」
ことも重要です。
今回は初級者向けに5つの問題を用意しましたので、ぜひチャレンジしてみてください!
対象読者
- React を使ったことがある初心者
- コンポーネントの設計に興味がある方
- リファクタリングに興味がある方
また、以下の記事を読んでおくと、今回の問題がより理解しやすくなるかもしれません。
この記事を読んだ後に身につくスキル
- React コンポーネントの設計に関する理解
- パフォーマンスの向上につながるコーディングスキル
- リファクタリングの基本的な考え方
それでは、問題にチャレンジしてみましょう!
問題1: 無駄な再レンダリング
以下のコンポーネントには、パフォーマンス上の問題があります。どこを、どのように改善すべきでしょうか?
function ExpensiveComponent({ data }) {
const processedData = heavyProcessing(data);
return <div>{processedData}</div>;
}
function ParentComponent() {
const [count, setCount] = useState(0);
const data = { /* 超絶大量のデータ */ };
return (
<div>
{/* カウントを増やす */}
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ExpensiveComponent data={data} />
</div>
);
}
考察のポイント:
-
ParentComponent
の状態が変更されるたびに、何が起こっているでしょうか? -
ExpensiveComponent
の処理を最適化するには、どうすればよいでしょうか?
さて、どうでしょうか?改善点が見つかりましたか?
自分なりの答えが出せたら、以下の「解説」を開いて、正解をチェックしてみてください。
解説
自信を持って答えを出せましたか?
それとも、「ちょっと自信ないな...」という感じでしょうか。
では、解説していきます!
この問題の主な課題は、ParentComponent
の count
状態が変更されるたびに、ExpensiveComponent
が不必要に再レンダリングされます。
そのたびに重い処理(heavyProcessing
)が毎回実行され、パフォーマンスが低下してしまいます。
改善方法は主に2つあります。
-
React.memo
を使ってExpensiveComponent
をメモ化し、props が変更されない限り再レンダリングされないようにする -
useMemo
を使ってheavyProcessing
の結果をメモ化し、data
が変更されない限り再計算されないようにする
これらを適用すると、以下のようになります。
const ExpensiveComponent = React.memo(({ data }) => {
// 重い処理を useMemo でメモ化
const processedData = useMemo(() => heavyProcessing(data), [data]);
return <div>{processedData}</div>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
// data を useMemo でメモ化して再計算を防ぐ
const data = useMemo(() => ({ /* 大量のデータ */ }), []);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ExpensiveComponent data={data} />
</div>
);
}
このように改善すると、count
の変更が ExpensiveComponent
の再レンダリングを引き起こさなくなり、パフォーマンスが大幅に向上します。
想像通りだったでしょうか?
もし正解できていたら、React Hooks の使い方に慣れてきている証拠かもしれませんね!
分かっていなかったとしても、この機会にぜひ覚えておいてください。
問題2: 不適切な useEffect
次の問題です。以下のコンポーネントには、useEffect
の使い方に問題があります。
どこが問題で、どう修正すべきでしょうか?
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ユーザーIDを元にユーザー情報を取得
useEffect(() => {
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => setUser(data));
});
if (!user) return <div>Loading...</div>;
// ユーザー名を表示
return <div>{user.name}</div>;
}
考察のポイント:
- この
useEffect
は、いつ、どのような頻度で実行されるでしょうか? - API リクエストの観点から、どのような問題が起こる可能性がありますか?
考えがまとまったら、解説を見てみましょう。
解説
さて、どうでしょうか。この問題の罠に引っかからなかったでしょうか?
この問題の主な課題は、useEffect
の依存配列が空
であることです。
これにより、コンポーネントが再レンダリングされるたびに useEffect
が実行され、不要な API リクエストが発生してしまいます。
これは、お客さんが「コーヒーをください」と言うたびに、新しいコーヒー豆を買いに行くようなものです。非効率どころか、お客さんを待たせてしまいますね。
改善方法は簡単です。
-
useEffect
の依存配列にuserId
を追加する
これを適用すると、以下のようになります。
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => setUser(data));
}, [userId]); // userId を依存配列に追加(=userId が変更されたら API を叩く)
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
この改善により、userId
が変更されたときだけ API リクエストが行われるようになります。
ユーザーIDが変わらない限り、不要なリクエストを送信することはありませんので効率的ですね!
コーヒーの例で言えば、お客さんが新しい種類のコーヒーを注文したときだけ、新しいコーヒー豆を買いに行くイメージです。
いかがでしたか?この問題は React 初心者がよく陥りがちな罠の一つです。
次の問題も、気を引き締めて挑戦しましょう!
問題3: コンポーネント間のデータ受け渡し
次の問題です。以下のコンポーネント構造には、データの受け渡し方に問題があります。
どこが問題で、どのように改善できるでしょうか?
function GrandParent({ user }) {
return <Parent user={user} />;
}
function Parent({ user }) {
return <Child user={user} />;
}
function Child({ user }) {
return <GrandChild user={user} />;
}
function GrandChild({ user }) {
return <div>{user.name}</div>;
}
function App() {
const user = { name: 'Alice' };
// ユーザー情報をPropsで渡す
return <GrandParent user={user} />;
}
考察のポイント:
-
user
オブジェクトはどのように渡されていますか? - 中間のコンポーネント(
Parent
やChild
)はuser
を実際に使用していますか? - この構造のデメリットは何でしょうか?
考えをまとめたら、解説を見てみましょう。
解説
3問目はいかがでしたか?
この問題で扱っているのは Prop Drilling
(プロップ ドリリング) という問題です。
Prop Drilling とは、深くネストされたコンポーネントにデータを渡すために、中間のコンポーネントを経由してプロップスを渡していくことを指します。
「バケツリレー」と呼ばれることがあります。
たとえば手紙を渡すとき、AさんからCさんに直接渡すのではなく、Bさんを経由して渡すようなイメージです。
これでは手間がかかりますよね。
Prop Drilling の主な問題点は以下の通りです。
- コードの可読性が下がる
- コンポーネントの再利用性が低下する
- 中間コンポーネントが不必要にプロップスに依存してしまう
この問題を解決する方法はいくつかありますが、最も一般的なのは React の Context API
(useContext) を使用することです。
// UserContext を作成
const UserContext = React.createContext();
function UserProvider({ children }) {
const user = { name: 'Alice' };
// UserContext.Provider で user データを提供
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
}
function GrandChild() {
// useContext を使ってデータにアクセス
const user = useContext(UserContext);
return <div>{user.name}</div>;
}
// 中間コンポーネントは user プロップスを受け取る必要がなくなる
function GrandParent() {
return <Parent />;
}
function Parent() {
return <Child />;
}
function Child() {
return <GrandChild />;
}
function App() {
return (
<UserProvider>
<GrandParent />
</UserProvider>
);
}
この改善により、user
オブジェクトを必要とするコンポーネントだけが useContext
を使ってデータにアクセスできるようになります。
中間コンポーネントは user
について知る必要がなくなり、コードがクリーンになりましたね。
手紙の例で言えば、直接受け取る人だけが手紙を受け取れるようになり、手紙の経由人が不要になったイメージです。
途中で手紙を受け取る人が変わっても、手紙の内容は変わらずに届くので、手間も省けます。
いかがでしたか?この問題は React の設計パターンに関するものでした。
ここを理解できていれば、それなりに規模が大きいアプリケーションでも、コンポーネントのデーソ受け渡しに悩むことは少なくなるでしょう。
問題4: 不要な状態管理
次は状態管理に関する問題です。以下のコンポーネントには、状態管理に関する問題があります。
どこが問題で、どのように改善できるでしょうか?
// 摂氏と華氏の温度を変換するコンポーネント(°C → °F, °F → °C)
function TemperatureConverter() {
// 摂氏と華氏の状態を管理
const [celsius, setCelsius] = useState('');
const [fahrenheit, setFahrenheit] = useState('');
// 摂氏が変更されたときに華氏を更新
const handleCelsiusChange = (e) => {
const value = e.target.value;
setCelsius(value);
setFahrenheit(((value * 9) / 5 + 32).toFixed(2));
};
// 華氏が変更されたときに摂氏を更新
const handleFahrenheitChange = (e) => {
const value = e.target.value;
setFahrenheit(value);
setCelsius(((value - 32) * 5 / 9).toFixed(2));
};
return (
<div>
<input
value={celsius}
onChange={handleCelsiusChange}
placeholder="Celsius"
/>
<input
value={fahrenheit}
onChange={handleFahrenheitChange}
placeholder="Fahrenheit"
/>
</div>
);
}
考察のポイント:
- このコンポーネントでは、どのような状態が管理されていますか?
- 摂氏と華氏の値は互いにどのように関連していますか?
- この実装方法のデメリットは何でしょうか?
考えをまとめたら、解説を見てみましょう。
解説
この問題の主な課題は、互いに依存関係のある2つの状態(celsius
と fahrenheit
)を別々に管理していることです。
これにより、以下のような問題が発生する可能性があります。
- 状態の不整合:一方の値を更新したときに、もう一方の値の更新を忘れる可能性がある
- 冗長なコード:両方の値に対して似たようなロジックを書く必要がある
- パフォーマンスの低下:2つの状態を更新するため、不必要な再レンダリングが発生する可能性がある
これは、料理のレシピを2つの別々のノートに書いているようなものです。
一方を更新しても、もう一方を更新し忘れる可能性があり、結果として情報の不一致が起こりやすくなります。
改善方法としては、1つの状態だけを管理し、もう一方の値は計算によって導出するようにします。
function TemperatureConverter() {
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('c'); // 'c' for Celsius, 'f' for Fahrenheit
// 摂氏と華氏を変換
const celsius = scale === 'f' ? ((temperature - 32) * 5 / 9).toFixed(2) : temperature;
const fahrenheit = scale === 'c' ? ((temperature * 9) / 5 + 32).toFixed(2) : temperature;
// 摂氏が変更されたときに華氏を更新
const handleCelsiusChange = (e) => {
setTemperature(e.target.value);
setScale('c');
};
// 華氏が変更されたときに摂氏を更新
const handleFahrenheitChange = (e) => {
setTemperature(e.target.value);
setScale('f');
};
return (
<div>
<input
value={celsius}
onChange={handleCelsiusChange}
placeholder="Celsius"
/>
<input
value={fahrenheit}
onChange={handleFahrenheitChange}
placeholder="Fahrenheit"
/>
</div>
);
}
この改善により、以下のメリットが得られます。
- 状態の一貫性が保たれる:1つの状態(
temperature
)のみを管理し、もう一方の値は常に計算によって導出される - コードの簡素化:変換ロジックが1箇所にまとまる
- パフォーマンスの向上:状態更新が1回で済むため、不要な再レンダリングを減らせる
レシピの例で言えば、1つのマスターレシピを持ち、必要に応じて量を変換するようなイメージです。
これにより、情報の一貫性が保たれ、更新も簡単になりますね。
いかがでしたか?この問題は React での効率的な状態管理に関するものでした。
React に関する知識だけでなく、一般的なプログラミングの設計にも通じる内容です。
さあ、最後の問題です。頑張りましょう!
問題5: PropsとStateの不変性
以下のシンプルなカウンターコンポーネントには、PropsとStateの扱いに関する重大な問題があります。どこが問題で、どのように改善できるでしょうか?
function Counter({ initialCount }) {
// カウントを増やす
const increment = () => {
initialCount++;
};
// カウントを減らす
const decrement = () => {
initialCount--;
};
return (
<div>
<p>Count: {initialCount}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
考察のポイント:
- このコンポーネントで、Propsはどのように扱われていますか?
- カウントの増減は正しく行われていますか?画面に反映されますか?
- Reactの「不変性」のルールに違反している箇所はどこですか?
考えをまとめたら、解説を見てみましょう。
解説
お疲れさまでした!最後の問題はいかがでしたか?
この問題の主な課題は、以下の2点です:
- Propsを直接変更しようとしている
- Stateを使用せずに値を変更しようとしている
具体的には、count
変数がinitialCount
Propsから直接初期化され、その後increment
とdecrement
関数内で直接変更されています。これはReactの大原則に反しています。
これは、誰かからもらった手紙の内容を直接書き換えようとするようなものです。元の内容は失われてしまいますし、他の人が同じ手紙を読むときに混乱を招きますよね。
Reactでこのような実装をすると、以下のような問題が発生します。
- 再レンダリングが行われないため、画面上の表示が更新されない
- コンポーネントの状態管理が正しく行われない
- Reactの単方向データフローの原則に違反する
改善方法としては、まず Props では受け取った値は useState
Hookを使ってStateとして管理しつつ、Stateの更新にはsetXxxx
関数を使用します。
function Counter({ initialCount }) {
// カウントをStateとして管理
const [count, setCount] = useState(initialCount);
const increment = () => {
// setCount 関数を使ってカウントを増やす
setCount(prevCount => prevCount + 1);
};
const decrement = () => {
// setCount 関数を使ってカウントを減らす
setCount(prevCount => prevCount - 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
この改善により、以下のメリットが得られます。
- Reactの不変性ルールを守ることができる
- 状態の変更が正しく画面に反映される
- コンポーネントの再レンダリングが適切に行われる
手紙の例で言えば、元の手紙はそのままに、新しい内容を別の紙に書くようなものです。元の内容は保持されつつ、新しい状態を作り出すイメージです。
いかがでしたでしょうか?
この問題はReactの基本的なルールの一つである「PropsとStateの不変性」と「適切な状態管理」に関するものでした。
非常に重要なポイントなので、しっかり理解しておきましょう!
まとめ
お疲れさまでした!
これらの問題を通じて、React コンポーネントの改善ポイントについて、理解を深めていただけたでしょうか?
今回の問題で扱ったポイントを簡単におさらいしてみましょう。
- 無駄な再レンダリングを防ぐ(メモ化の活用)
- useEffect の適切な使用(依存配列の重要性)
- Prop Drilling の回避(Context API の活用)
- 効率的な状態管理(単一の信頼できる情報源)
- PropsとStateの不変性(Reactの基本原則の遵守)
これらのポイントを意識して、コンポーネントを設計することで、より保守性の高いコードを書くことができます。
今回の問題が、React コンポーネントの設計に関する理解を深めるきっかけになれば幸いです!
ちょっと宣伝
Qiita では主に「React をちょっと書ける人向け」記事を書いていますが、個人ブログでは「React をこれから学びたい人向け」の記事も書いてます。
もし興味があれば覗いていただけると感謝感激です。