React。WebフロントエンドのUI開発で使われる、JavaScriptライブラリ。
そのシンプルさや簡潔さで、近年は人気が非常に高まっています。
そんなReactですが、いざ作ってみると、全然思った通りに動かない、ということが起こります。
特に、これまでUI開発をこなしていたベテランほど、泥沼にハマったり。
Reactにある、これまでのUI開発とは異なる思想が、けっこう罠になるのです。正しい理解を身に着けて、適切なReactコードを書きましょう。
こんなReactコードは動かない
まずは、UI開発経験者がやりがちな、「動かないReactコード」の例です。
サンプル作ってみたけれど…
Reactで開発することになったあなた。
チュートリアルでさくっと勉強。関数コンポーネント?JSX?フック?ステート?…まあやってみれば分かるでしょう。
とりあえず簡単なサンプル書いてみよう。
入力内容に書いたテキストを、大文字にして、変換結果に表示する、こんなイメージ。
inputText
のonChange
イベントで出力しよう。こんな感じかな?
function App() {
let inputText = "";
let convertedText = "";
const handleChange = (e) => {
const newText = e.target.value;
inputText = e.target.value;
convertedText = 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
よし動かしてみよう。
…あれ、画面が全然動かない。入力すらもできないぞ?なんで?
UI開発経験者が考えていること
このコード、ベテラン開発者が見ると、こんな雰囲気で見てるかも。
- Appクラス(っぽい)画面の枠組みを作って
- inputTextとかクラスのメンバ(っぽい)入力値を定義して
- 入力イベントで値を操作して
- HTML(っぽい)結果を返して
ぱっと見、これまで作ってきた、クラスベースのUIコードと同じっぽい。
でも動かない。動かない理由がよく分からない…
経験者ほど陥りがち、Reactに対する誤解
さて、このコード、なぜ動かないのでしょうか?
まあ問題は、「っぽい」というところですね。ここを捉え間違えています。
ポイントは、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が、関数コンポーネントAppを呼び出す
function App() {
inputText
,convertedText
に空文字が設定される
let inputText = "";
let convertedText = "";
handleChange
は内部関数定義だからなにもしない- 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が
handleChange
を呼ぶ handleChange
の中身が実行される
const handleChange = (e) => {
const newText = e.target.value;
inputText = e.target.value;
convertedText = newText.toUpperCase();
}
…以上。
Reactは関数コンポーネントAppを呼ばないし、描画関数が返却されることもない。
なので、画面が全く更新されない、というわけです。
正しいReactコードにしてみよう
いやいやReactさん、なんでApp呼んでくれないの?どう実装すりゃいいのよ?
ここで登場するのが、Reactの肝、フックの仕掛けです。
フックを使って書き換える
フックを使って、正しいコードに書き換えてみましょう。
React Hooksのひとつ、useStateを使って、コードを置き換えます。
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
変わったところは、
useState
ってやつで、なんかset~
っての作って- 設定時に
set~
ってのを使ってる
それだけ。でもこれで、ちゃんと動くようになります。なんで?
React Hooksとは何か?
Reactの肝であり(そして難解な)React Hooks。いったい何なのでしょう?
よくこういう説明がされます。
React Hooksとは、状態やライフサイクルをReactで管理するための機能です
うーん分からん。
実際、フックは実際に動かして体験しないと、ピンとこないところあります。
動きを確認して、感じを掴みましょう。
処理の流れを追ってみよう
それでは、フックで書き換えた処理の、流れを追ってみましょう。
初期表示
初期表示の流れです。
- Reactが、関数コンポーネントAppを呼び出す
function App() {
useState
で、変数とそのset関数を取得する。初回呼び出しなので、初期値(空文字)が設定される
const [inputText, setInputText] = useState("");
const [convertedText, setConvertedTest] = useState("");
handleChange
は内部関数定義でなにもしない- 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が
handleChange
を呼ぶ handleChange
の中身が実行される
const handleChange = (e) => {
const newText = e.target.value;
setInputText(newText);
setConvertedTest(newText.toUpperCase());
}
- set~の呼び出しで、
inputText
とconvertedText
が変更されたことを、Reactに通知する handleChange
が終わると、Reactは、stateが変更された関数コンポーネントを調べる- Reactは、Appが変更されたことを検知し、Appを呼び出す
function App() {
- useStateで、変数とそのset関数を取得する。2回め呼び出しなので、前回のsetで設定した値が入る
const [inputText, setInputText] = useState("");
const [convertedText, setConvertedTest] = useState("");
- JSXで定義された描画関数が、リターンされる(前回のsetで設定した値が反映されている)
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を描画する
今回は、無事にAppが呼ばれ、入力された値で画面が反映されました。
Reactの重要な概念「リアクティブ」
この独特の仕掛けは、「リアクティブプログラミング」と言われる思想に則っています。
「データ変更」をReactに通知する
先ほどの処理のポイントはuseState
。これが持つ役割は、この通り。
useState
で返却されるset関数は、
- 単に変数をセットするためのものではない
- Reactに「データを変更したから、このコンポーネントは再描画が必要」と伝えるためのもの
ユーザーは、関数コンポーネントで描画の定義を書きます。しかし、それをユーザー側が直接呼ぶことはありません。
描画をするのは、あくまでReact側です。ユーザープログラムは、Reactにその定義を渡しているだけなのです。
フックで「コンポーネントをReactに結びつける」
このuseStateは、React Hooksと呼ばれるReactの仕掛けです。
フックは他にも色々あります。よく使われるのはuseState
ですが、他にもこのようなものがあります。
- useState
- useReducer
- useEffect
- useContext
- useCallback
- useMemo
用途は様々ですが、いずれも関数コンポーネントの変化を通知する、という役割は同じです。
Reactに、自分が作った関数コンポーネントを結びつける(フックする)。それがReact Hooksの役割です。
データ変更のタイミングで処理を動かす「リアクティブ」
データが変更されたら、自動的にReactが描画する。
なのでユーザーは、データと描画の定義を宣言するだけでよい。
この考えが「リアクティブプログラミング」。これまでにはみられなかった、UI開発の新しい概念です。
実は、Reactの名前の由来は、この“Reactive”から来ているのです。
Reactプログラミングで意識すべきこと3つ
リアクティブな考えでプログラミングするReact。これまでのUIプログラミングとは異なる注意ポイントがあります。
特に、以下の3つは意識しましょう。
- あらゆる状態をフックで管理する
- 状態の冗長性や矛盾を取り除く
- コンポーネントを純粋にする
あらゆる状態をフックで管理する
データが変更されたら、Reactが自動的に描画を行います。そしてどこを描画するかは、フックによって判断します。
つまり、画面描画に関わるすべての状態は、フックで管理する必要があります。
自前で定義したグローバル変数などは使わず、あらゆるデータの状態をフックに登録しましょう。
状態の矛盾や冗長性を取り除く
例えば、メッセージを表示するラベルがあり、「警告の場合だけ、メッセージを赤色かつ太字で表示する」とします。
画面描画に関わる状態をフックで管理するとして、
- メッセージ色(黒色/赤色)
- メッセージフォント(通常/太字)
の2つで管理するのは、間違ってはいないが冗長でしょう。
- 警告状態(警告なし/あり)
として管理すれば、メッセージ色もフォントも一意に定まります。
できるだけ、管理する状態の定義をシンプルに保ちましょう。画面との対応が取りやすく、バグも少なくなります。
コンポーネントを純粋にする
純粋な関数であるとは、「その関数を何回呼び出しても同じ結果になる」ということ。冪等(べきとう)性とも呼ばれます。
Reactの動き、実際に作ると分かるのですが、
- 関数コンポーネントは、どんなタイミングで呼び出されるか分からない
- 関数コンポーネントが、何回も呼び出される時もある
といった挙動を示します。
これはReactが、関数コンポーネントが純粋である、ということを信じて呼び出しているから。
関数コンポーネントを作るときは、純粋性を保つように作りましょう、
具体的に言えば、「フックの値が変わらない限り、必ず同じ結果になる関数コンポーネント」を意識します。
ちなみに、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のメリットを存分に活用しましょう。
コメント