はじめに
自分は2021年に新卒でWeb系の開発会社にフロントエンジニアとして入社し2022年で2年目になります。
実務ではReact×TypeScriptを利用したフロント周りの開発をメインで行なっていなす。
今回は、現場で後輩に質問されたReactの技術質問をまとめていきます。
なお質問に対しては一問一答形式で答えるのではなく、深ぼって解説をしていきます。
この記事の対象者
- フロントエンジニアを目指している人
- React初心者から中級者
- Reactの質問をされた時にうまく言語化できない人
この記事の目標
- Reactでよく使われている技術を言語化できるようになる
- 何となくの理解から脱却する
おことわり
- 本記事は面接等で聞かれる質問テンプレート集ではありません
- 現場で後輩に聞かれた質問を深ぼって解説をするノリで書いてます
Reactフックとは何か?
Reactフックは公式ドキュメントにおいて下記のように解説されています。
フック (hook) は React 16.8 で追加された新機能です。state などの React の機能を、クラスを書かずに使えるようになります。
ここではReactフックの中のuseState
とuseEffect
について詳しく解説していきます。
useStateの役割と動きについて教えてください
useState
はReact内部で状態を管理することができるフックです。
公式ドキュメントでは下記のように定義されています。
ステートフルな値と、それを更新するための関数を返します。
setState 関数は state を更新するために使用します。新しい state の値を受け取り、コンポーネントの再レンダーをスケジューリングします。
useState
の流れについて少し噛み砕くと、下記のようになります。
-
useState
はReact内部と接続し状態を保持できるようにする -
useState
は現在の値と、値を更新するための関数(更新関数)を返す -
useState
の更新関数に新しい値を渡すと、Reactに対してコンポーネントを再実行(再レンダリング)するように命令する
具体的にReactのコードから流れを追っていきます。
const Practice: NextPage = () => {
const [value, setValue] = useState<string>("初期値");
const onClick = () => {
setValue("stateを更新しました");
};
console.log("レンダリング");
return (
<>
<p>{value}</p>
<button onClick={onClick}>stateを更新</button>
</>
);
};
- ページが読み込まれたタイミングで初回レンダリング
-
useState
を使ってvalueという状態をReactで管理(初期値有り) - ボタンをクリックすると新しい値が更新関数に渡される
- 更新関数に渡された値をReactに渡し、Reactコンポーネントは再レンダリング
- 画面が更新され3で渡した新しい値が表示される
useEffectの役割と動きについて教えてください
useEffect
は公式ドキュメントで下記のように解説されています。
このフックを使うことで、レンダー後に何かの処理をしないといけない、ということを React に伝えます。React はあなたが渡した関数を覚えており(これを「副作用(関数)」と呼ぶこととします)、DOM の更新の後にそれを呼び出します。この副作用の場合はドキュメントのタイトルをセットしていますが、データを取得したりその他何らかの命令型の API を呼び出したりすることも可能です。
少し噛み砕くと、useEffect
を使うことでレンダリング結果が画面に反映された後にuseEffect
内に記述された副作用の処理等を実行することができます。
ここでいう副作用とは下記のような処理のことを指します。
- DOMの書き換え操作
- サーバー通信(APIコール)
- 変数の書き換え
useEffect
は第一引数にレンダリング時に実行したい関数を指定し、第二引数に第一引数で指定した関数を制御するための依存データを入れます。
【初回レンダリング時に「Hello World」がログに出力される処理】
export const Parent: React.FC = () => {
useEffect(() => {
console.log("Hello World");
}, []);
return (
<>
<p>テスト</p>
</>
);
};
上記のようにuseEffect
の第二引数の依存データに空の配列を渡すと初回レンダリング時のみ、第一引数に記述した関数を実行することができます。
次に第二引数の依存配列に値を指定した場合を考えます
【countが増えるごとにuseEffect内部の処理が実行される】
export const Parent: React.FC = () => {
const [count, setCount] = useState<number>(0);
const addCount = () => {
setCount(count + 1);
};
useEffect(() => {
console.log("カウントが増えました");
}, [count]);
return (
<>
<button onClick={addCount}>クリック</button>
<p>{count}</p>
</>
);
};
この場合ボタンをクリックするたびに、useEffect
の第二引数で指定したcountが増え、useEffect内の第一引数で指定した処理が実行されることが確認できます。
状態管理の更新について
次に先ほど紹介したuseState
においてのよくある質問を見ていきます。
配列やオブジェクトのstateを更新するにはどうすればよいですか?
配列やオブジェクトのstate更新は初心者が詰まることが多いので詳しく見ていきます。
下記の例を元に説明します。
- 配列のfruitsというstateを用意する
- ボタンをクリックすると「ぶどう」という文字列が追加され状態を更新する
【配列の更新がうまくいかない場合】
import React, { useState } from "react";
export const Practice: React.FC = () => {
const [fruits, setFruits] = useState<string[]>(["りんご", "ばなな"]);
// 配列の最後にぶどうを追加する処理
const onClick = () => {
fruits.push("ぶどう");
setFruits(fruits);
};
return (
<>
<p>{fruits}</p>
<button onClick={onClick}>ぶどうを追加</button>
</>
);
};
この場合、ボタンをクリックしても画面に表示される値は更新されません。
Reactコンポーネントが再レンダリングされていないことがわかります。
その理由は、公式ドキュメントのuseStateを確認すると
state 更新の回避
現在値と同じ値で更新を行った場合、React は子のレンダーや副作用の実行を回避して処理を終了します(React は Object.is による比較アルゴリズムを使用します)。
state
を更新する関数には「新しいstate」を入れることで再レンダリングが実行されると書いてあり、今回の場合は既存のstate
に直接値をpush
しているので、同じ値として判定されレンダリングが回避されています。
const onClick = () => {
// 直接stateに値をpushしても同じ値と検知される
fruits.push("ぶどう");
// 結果、更新関数を使ってもレンダリングが起きない
setFruits(fruits);
};
スプレット構文を使って既存配列のcopyを取り、更新関数に受け渡すことでレンダリングが発火されます。
【配列の更新がうまくいく場合】
const onClick = () => {
// スプレット構文で既存の値をコピー
const copy = [...fruits];
// コピーに対して値を追加
copy.push("ぶどう");
// 既存のstateとは異なる値(新しい値)が入ってくるのでレンダリングが起きる
setFruits(copy);
};
ちなみにこちらの処理は下記のように書くこともできます。
const onClick = () => {
setFruits([...fruits, "ぶどう"]);
};
なお下記の記事でより詳しくこの部分に関しては解説をしているので読んでいただけると嬉しいです。
配列とkeyについて
配列をループ処理でJSX内で使う場合にkeyを指定する理由は?
下記のように配列をjsx内で利用する場合にkeyを受け渡している理由について解説していきます。
【ボタンをクリックすると配列の先頭にみかんが追加される】
import React, { useState } from "react";
import { Fruits } from "./fruits";
export const Practice: React.FC = () => {
const [fruits, setFruits] = useState<string[]>([
"りんご",
"ばなな",
"ぶどう",
]);
const onClick = () => {
const copy = [...fruits];
copy.unshift("みかん");
setFruits(copy);
};
return (
<>
{fruits.map((i) => (
<div key={i}>
<Fruits name={i} />
</div>
))}
<button onClick={onClick}>みかんを追加</button>
</>
);
};
配列とkeyについて公式ドキュメントでは下記のように説明されています
Key は、どの要素が変更、追加もしくは削除されたのかを React が識別するのに役立ちます。配列内の項目に安定した識別性を与えるため、それぞれの項目に key を与えるべきです。
兄弟間でその項目を一意に特定できるような文字列を key として選ぶのが最良の方法です。多くの場合、あなたのデータ内にある ID を key として使うことになるでしょう
keyを指定する理由については公式ドキュメントでは、下記のように解説されています。
デフォルトでは、DOM ノードの子に対して再帰的に処理を行う場合、React は単純に、両方の子要素リストのそれぞれ最初から同時に処理を行っていって、差分を見つけたところで毎回更新を発生させます。
この部分を図を用いて噛み砕いて解説します。
ReactはReactの要素ツリーの差分を検知してDOMの更新をしています。そのため、レンダリング時の差分を見てブラウザで反映されます。
keyを渡さない場合はReactでは全てのReact要素を差分として認識し、DOMに反映する。
差分は「みかん」が一番前に追加されただけなのに、それに伴って「りんご、ばなな、ぶどう」の位置が変わったことで全部が差分として認識されてしまっている。
一方でkeyを渡すことで、下記のようにReactツリーの位置が変更しても識別され差分だけが抽出され更新される。
以上より、keyを指定しないと変更していない箇所も差分として認識されパフォーマンスが悪くなってしまいます。
またkeyに指定する値はindexよりも一意に識別できる値(ID等)を指定するのが推奨されています。
この部分の理由に関しては下記の記事を参考にしてみてください。
DOMの操作方法について
DOMを操作するにはどうすればいいですか?
useRef
を使うことでDOM操作をすることができます。
公式ドキュメントではuseRef
は下記のように説明されています。
useRef は、.current プロパティが渡された引数 (initialValue) に初期化されているミュータブルな ref オブジェクトを返します。返されるオブジェクトはコンポーネントの存在期間全体にわたって存在し続けます。
本質的に useRef とは、書き換え可能な値を .current プロパティ内に保持することができる「箱」のようなものです。
具体例をもとに少し噛み砕いて説明します。
【JSX内のinputのを取得し変数に格納】
import React, { useRef } from "react";
export const Practice: React.FC = () => {
const inputRef = useRef(null);
console.log(inputRef);
return (
<>
<input ref={inputRef} type="text" value={"refです"} />
</>
);
};
console
を確認すると、下記のようにinput
のDOMが取得されていることを確認できます。
これを元に下記の処理のコードを書いてきます。
【ボタンをクリック時に入力フォームにフォーカスする処理】
export const Practice: React.FC = () => {
// inputを取得する用
const inputRef = useRef<any>();
// ボタンをクリック時にinputにフォーカスを合わせる関数
const onClick = () => {
inputRef.current.focus();
};
return (
<>
<button onClick={onClick}>入力フォームにフォーカス</button>
<br></br>
<input ref={inputRef} type="text" onClick={onClick} />
</>
);
};
実際にボタンをクリックすると入力フォームにフォーカスが当たることを確認できます。
useRef
とuseState
の違いは、
- useaRefは再レンダリングされずにデータを保持する
- Refが変更しても再レンダリングされない(中に入っているデータは変わっている)
- useRefはDOM要素にrefを渡す際に用いられる
関数型プログラミングって何ですか?
Reactを使う上で知っておくべき概念である、関数型プログラミングについて解説をしていきます。
関数型プログラミングには下記の3つの特徴があります。
- 状態管理と処理の分離
- 純粋関数
- 不変性(immutability)
一つずつ詳しく見ていきます。
状態管理と処理の分離
関数型プログラミングでは、状態管理と処理を分類して管理します。
具体的にReactのコードを見ながら確認します。
【+1ボタンを押すとcountという状態に1が加算される】
import React, { useState } from "react";
export const Practice: React.FC = () => {
const [count, setCount] = useState<number>(0);
const addCount = () => {
setCount(count + 1);
};
return (
<>
<p>{count}</p>
<button onClick={addCount}>+1</button>
</>
);
};
count
というstate
をReact内部に保持(状態管理)し、関数コンポーネントであるPractice
がJSXを返す(処理)を分離している。
あくまでstate
の更新や管理はReact内部で行われており、関数コンポーネント(Practice
)からは切り離されている。
純粋関数
関数型プログラミングの2つめの特徴である純粋関数について説明します。
純粋関数には下記の特徴があります。
- 引数が同じ値なら返り値は常に同じ値になる
- 関数外の状態は参照・更新しない(副作用が発生しない)
- 引数で渡ってきた値を更新しない
まずは上記を満たさない場合を確認していきます。
【関数外の状態を参照・更新している場合(副作用が発生している)】
let count = 0;
export const Child: React.FC = () => {
// 関数外の変数を更新
count++;
return <p>{count}</p>;
};
export const Parent: React.FC = () => {
return (
<>
<Child />
<Child />
</>
);
};
関数外で定義されているvalue
という変数をChildコンポーネント内部で参照及び更新をしている。
その結果、Parentコンポーネントでは同じChildコンポーネントを呼び出しているのに、呼び出す順番によって別の結果が返ってきてしまっていることがわかります。
このように純粋関数でない場合、思わぬバグを生み出してしまう可能性が出てしまいます。
【純粋関数である場合】
type ChildProps = {
count: number;
};
export const Child: React.FC<ChildProps> = ({ count }) => {
return <p>{count}</p>;
};
export const Parent: React.FC = () => {
return (
<>
<Child count={0} />
<Child count={1} />
<Child count={1} />
</>
);
};
この場合はChild
コンポーネントにおいて、同じ引数(count
)を渡すと同じ結果が返ってきていることを確認できます。
この場合のChildコンポーネントは純粋関数であるといえます。
このようにReactコンポーネントは純粋関数で定義することで、思わぬバグを防ぐことができます。
不変性(immutability)
不変性(immutability)の特徴は、下記のようになっています。
- 引数で渡された値は変更しない
【不変性(immutability)でない場合】
type ChildProps = {
count: number;
};
export const Child: React.FC<ChildProps> = ({ count }) => {
// 引数で渡ってきた値を更新している
count = 30;
return <p>{count}</p>;
};
export const Parent: React.FC = () => {
return (
<>
<Child count={0} />
<Child count={1} />
<Child count={2} />
</>
);
};
props
で渡ってきたcount
という値をChildコンポーネント内部で更新しているので、この場合は**不変性(immutability)**ではない関数になっています。
関数型プログラミングで書くことのメリット
関数型プログラミングをまとめると、
- 状態管理と処理を切り離す
- 純粋関数で副作用を排除する
- 不変性
このような関数型プログラミングにすることで
- 可読性が上がる
- 再利用性がある
- テストが書きやすい
- モジュール化しやすい
といったメリットを得ることができます。
最後に
いかがだったでしょうか。
今回はReactの技術質問についてまとめました。
現場ではReacで開発をしているが、技術的な質問された時にうまく言語化できなかったので、改めて整理しなおしました。
基本的な内容についての解説だったので、次回の記事では下記のような内容を書こうかと思っています。
- パフォーマンスの最適化
- グローバルな状態管理について
- Next.js関連
他にもフロントエンド周りの記事を書いているので読んでいただけると嬉しいです。