React。WebフロントエンドのUI開発で使う、人気のJavaScriptライブラリです。
そんなReactの神髄が、React Hooks(フック)を使った関数コンポーネントの思想です。
ところが、このReactフック、これまでと大きく異なるその思想が、けっこう難解です。
実例で動きを確認しながら、Reactフックの仕組みを理解しましょう。
Reactフックの実例を見てみよう
Reactフックとは一体なんなのでしょう?一般的にこのような説明がなされます。
…分からないですね。実例でイメージしてみましょう。
簡単なReactアプリで考えよう
題材として、このような単純なアプリを、Reactで実装してみましょう。
入力内容に書いたテキストを、大文字にして変換結果に表示する、
こんな感じですね。
import { useState } from 'react'
function App() {
const [inputText, setInputText] = useState("");
const [convertedText, setConvertedTest] = useState("");
const handleChange = (e) => {
const newText = e.target.value;
setInputText(newText);
setConvertedTest(newText.toUpperCase());
}
return (
<form>
<div>
<label>入力内容</label>
<input type="text" value={inputText} onChange={handleChange}/>
</div>
<div>
<label>変換結果</label>
<input readOnly type="text" value={convertedText}/>
</div>
</form>
)
}
export default App
特徴的なのがこの部分。Reactフックの仕掛けです。
const [inputText, setInputText] = useState("");
const [convertedText, setConvertedTest] = useState("");
const handleChange = (e) => {
const newText = e.target.value;
setInputText(newText);
setConvertedTest(newText.toUpperCase());
}
useState
で変数を取って、setInputText
で設定して…
…一見、ただ変数を設定しているだけ、な処理に見えます。
でも、このフックの仕掛けが無いと、画面が全然動きません。
なんでなんでしょう?
まず「関数コンポーネント」を理解する
Reactフックを理解するために、まずReactの「関数コンポーネント」の概念を押さえておきましょう。
Reactのコンポーネントは「関数」である
全体の枠組みである、App。これをReactではコンポーネントと呼びます。
定義はこう。
function App() {
}
Appとか、名詞だし、先頭が大文字だし、いかにもクラスっぽい名前。
でもfunction
とありますね。Reactコンポーネントの実体は「関数」です。
まずはこれが大前提。
戻り値のJSXも「関数」である
「関数」であるReactコンポーネント。returnで戻り値を返します。
returnの内容はこの部分。JSXと呼ばれる構文です。
return (
<form>
<div>
<label>入力内容</label>
<input type="text" value={inputText} onChange={handleChange}/>
</div>
<div>
<label>変換結果</label>
<input readOnly type="text" value={convertedText}/>
</div>
</form>
)
HTMLっぽいですね。でも勘違いしてはいけません。
ここで、HTMLテキストをreturnしているわけではないのです。
このJSX部分の正体は、HTMLを描画する「関数」。
JSXは、実際にはこんな感じで、HTMLを描画するJavaScript関数にコンパイルされます。
const header = <header>
<h1>Mozilla Developer Network</h1>
</header>;
const header = React.createElement(
"header",
null,
React.createElement("h1", null, "Mozilla Developer Network"),
);
出典:MDN Web Docs
「関数を返す」というのは、JavaScript経験者でなければ、ピンとこないかも。
JavaScriptでは、関数もオブジェクト。なので「関数が関数を返す」ということも普通にできるのです。
関数コンポーネントは「描画関数を返す関数」
Reactコンポーネントの正体は「描画する関数を返す関数」です。
そして、その描画関数は、React本体が呼び出します。(ユーザープログラムが呼び出すわけじゃありません)
Reactは、
- 関数コンポーネントを呼び出し
- 描画関数が戻ってきたら、それを呼びだして、HTMLを描画する
ということをしているのです。
関数コンポーネントでのフックの動きを追う
ここまでを踏まえて、どういう動きをしているのか、流れを追ってみましょう。
Reactフックのひとつ、useStateの動きに注目です。
初期表示
まず一番最初の表示です。
- Reactが、関数コンポーネントAppを呼び出す
function App() {
useState
で、変数とset関数を取得する。useState初回呼び出しなので、初期値(空文字)が設定される
const [inputText, setInputText] = useState("");
const [convertedText, setConvertedTest] = useState("");
handleChange
は内部関数定義だからなにもせずに飛ばす
- Appの戻り値で「JSXで定義された描画関数」がリターンされる
return (
<form>
<div>
<label>入力内容</label>
<input type="text" value={inputText} onChange={handleChange}/>
</div>
<div>
<label>変換結果</label>
<input readOnly type="text" value={convertedText}/>
</div>
</form>
)
- Reactが、描画関数を呼んで、HTMLを描画する
これで、初期表示は、中身が空で表示されます。
正確にいうと…
Reactは、厳密に言えば描画関数で直接HTMLを描画しているわけではありません。
Reactはまず仮想DOMに書き込み、そのあと実際のDOMとの差分を描画することで、レンダリングの高速化を図っています。
ただイメージとしては、描画関数でHTMLを描画すると思っておけば問題ないでしょう。
テキスト入力
次に、テキストを入力したときの流れです。
入力内容テキストに、”a”‘ を入力したとします。useStateの動きに注目しましょう。
- Reactが入力イベントを検知して
handleChange
を呼ぶ
handleChange
の中身が実行される
const handleChange = (e) => {
const newText = e.target.value;
setInputText(newText);
setConvertedTest(newText.toUpperCase());
}
- set~の呼び出しで、
inputText
に”a”、convertedText
に”A”を設定したことを、Reactに通知する
このイベント処理が終わった後、Reactがひと仕事します。
handleChange
が終わると、Reactは、stateが変更された関数コンポーネントを調べる- Reactは、Appが変更されたことを検知し、Appを呼び出す
function App() {
- useStateで、変数とそのset関数を取得する。前回のsetで設定した値”a””A”が入る
const [inputText, setInputText] = useState("");
const [convertedText, setConvertedTest] = useState("");
- 「JSXで定義された描画関数」(テキストに“a””A”が設定されている)が、リターンされる
return (
<form>
<div>
<label>入力内容</label>
<input type="text" value={inputText} onChange={handleChange}/>
</div>
<div>
<label>変換結果</label>
<input readOnly type="text" value={convertedText}/>
</div>
</form>
)
- Reactが、描画関数を呼んで、HTMLを描画する
これで、入力された値で、画面が反映されることになります。
フックの役割は「レンダリング」をReactに促すこと
復習しましょう。フックによる大きく動きが変わったのは、ここです。
const handleChange = (e) => {
const newText = e.target.value;
setInputText(newText);
setConvertedTest(newText.toUpperCase());
}
- set~の呼び出しで、inputTextに”a”、convertedTextに”A”を設定したことを、Reactに通知する
handleChange
が終わると、Reactは、stateが変更された関数コンポーネントを調べる- Reactは、Appが変更されたことを検知し、Appを呼び出す
そう、useStateフックで提供されるset関数は、ただ変数をセットするだけの関数ではないのです。
このset関数は、Reactに対して再レンダリングを促すために、使うのです。
※描画のことを、一般的に「レンダリング」と呼びます。
useStateでよくある勘違い
useStateが返すsetInputText関数。
set~という名前で勘違いされがちですが
- setInputTextで”a”を設定する意味は、次の再レンダリングにそれを使う、ことを意味します
なので
- setInputTextをしたとして、その場で即時にinputTextの中身が変わるわけではありません
- inputTextが”a”に変わるのは、Reactが関数コンポーネントを再度呼び出した、useStateのタイミングです
動きをちゃんと理解していれば、理解できますね。
Reactの重要な概念「リアクティブ」
Reactのこの独特の仕掛けは、「リアクティブプログラミング」という思想に則っています。
「データ変更」をReactに通知する
先ほどの処理のポイントはuseState
。役割はこの通り。
useState
で返却されるset関数は、
- 単に変数をセットするためのものではない
- Reactに「データを変更したから、このコンポーネントは再レンダリングが必要」と伝えるためのもの
ユーザーは、関数コンポーネントでレンダリングの定義を書きます。、が、ユーザー側で直接実行はしません。
レンダリングを実行するのは、あくまでReact側。
ユーザープログラムは、Reactにレンダリング用の定義(関数コンポーネント)を渡しているだけなのです。
フックで「コンポーネントをReactに結びつける」
Reactフックの大事な役目が、これです。
ここで使ったuseState
は、Reactフックのひとつで、一番よく使われます。
フックは他にも、このようなものがあります。
- useState
- useReducer
- useEffect
- useContext
- useCallback
- useMemo
用途は様々ですが、いずれも、関数コンポーネントの変化をReactに伝えるという役割は同じです。
フックの種類について
フックの種類により、いろいろな役割があります
useState useReducer useContext | 再レンダリングが必要なことをReactに通知する |
useEffect | 再レンダリングにより発生する副作用を定義する (再レンダリング時の外部システムへの通知など) |
useCallback useMemo | 再レンダリングが不要なことをReactに通知する (パフォーマンス向上に使う) |
役割は違いますが、いずれも再レンダリングに関する制御を受け持っていることが、分かりますね。
データ変更のタイミングで処理を動かす「リアクティブ」
- データが変更されたら、自動的にReactが描画する
- なのでユーザーは、データと描画の定義を宣言するだけでよい
この考えが「リアクティブプログラミング」。これまでにはみられなかった、UI開発の新しい概念です。
Reactプログラミングで意識すべきこと3つ
リアクティブな考えでプログラミングするReact。これまでのUIプログラミングとは異なる注意ポイントがあります。
特に、以下の3つは意識しましょう。
- あらゆる状態をフックで管理する
- 状態の冗長性や矛盾を取り除く
- コンポーネントを純粋にする
あらゆる状態をフックで管理する
データが変更されたら、Reactが自動的にレンダリングを行います。そしてどこを描画するかは、フックによって判断します。
つまり、レンダリングに関わるすべての状態は、フックで管理する必要があります。
自前で定義したグローバル変数などは、使わないようにしましょう。あらゆるデータの状態を、フックに登録するのです。
状態の矛盾や冗長性を取り除く
さきほどのコードをもう一度見てみます。このコードでは、
- inputText
- convertedText
の2つのstateを管理しています。
でも、実はこれ、冗長な状態管理なのです。convertedTextは、毎回inputTextから変換すればいいだけなので、2つも管理する必要がない。
こんな風に書き換えることができます。
import { useState } from 'react'
function App() {
const [inputText, setInputText] = useState("");
const handleChange = (e) => {
const newText = e.target.value;
setInputText(newText);
}
return (
<form>
<div>
<label>入力内容</label>
<input type="text" value={inputText} onChange={handleChange}/>
</div>
<div>
<label>変換結果</label>
<input readOnly type="text" value={inputText.toUpperCase()}/>
</div>
</form>
)
}
export default App
こうすると、管理するstateは、inputTextのひとつだけとなりますね。
できるだけ、管理する状態の定義をシンプルに保ちましょう。画面との対応が取りやすく、バグも少なくなります。
コンポーネントを純粋にする
関数コンポーネントを「純粋」に保ちましょう。
純粋とは「その関数を何回呼び出しても同じ結果になる」ということ。冪等(べきとう)性ともいいます。
Reactの動き、実際に作ると分かるのですが、
- 関数コンポーネントは、どんなタイミングで呼び出されるか分からない
- 関数コンポーネントが、何回も呼び出される時もある
これはReactが、関数コンポーネントが純粋である、ということを信じて呼び出しているから。
関数コンポーネントを作るときは、純粋性を保つように作ります。
具体的に言えば、「フックの値が変わらない限り、必ず同じ結果になる関数コンポーネント」を意識します。
純粋さを確認するStrictモード
ちなみに、ViteなどのReactテンプレートでコードを作った場合、Strictモードが実装されている場合があります。
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
これはデバッグ用の機能であり、Strictモードが有効の場合、Reactはコンポーネントを二度呼び出します。
純関数であれば保たれていれば、1度呼んでも2度呼んでも、結果は同じはず。Strictモードはそんな純粋性が保たれているかをチェックする機構です。
Strictモードでも正常に動くような関数コンポーネントを作りましょう。
まとめ
Reactが持つ「リアクティブ」の概念。旧来のデスクトップUI開発者や、Query開発者には、初めは奇妙に映るでしょう。
でも、ひとたびそのリアクティブ思想に慣れれば、非常に効率よく堅牢なUIアプリの開発が可能になります。
Reactを使いこなす近道は、実際にサンプルプログラムを作ってみること。
React公式サイトや、MDN Web Docsに、Reactチュートリアルがあります。こういったサイトも参考にしてみましょう。
その本質をきちんと理解して、Reactのメリットを存分に活用しましょう。
コメント