【実例で解説】Reactフックは何をするのか?その真髄を理解する

react-behavior

React。WebフロントエンドのUI開発で使う、人気のJavaScriptライブラリです。

そんなReactの神髄が、React Hooks(フック)を使った関数コンポーネントの思想です。

ところが、このReactフック、これまでと大きく異なるその思想が、けっこう難解です。

実例で動きを確認しながら、Reactフックの仕組みを理解しましょう。

目次

Reactフックの実例を見てみよう

Reactフックとは一体なんなのでしょう?一般的にこのような説明がなされます。

React Hooksは、関数コンポーネントでステート管理やライフサイクルの制御などを可能にする特別な関数です。

…分からないですね。実例でイメージしてみましょう。

簡単な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関数にコンパイルされます。

JSXの置き換え
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の動きに注目です。

初期表示

まず一番最初の表示です。

  1. Reactが、関数コンポーネントAppを呼び出す
function App() {

  1. useStateで、変数とset関数を取得する。useState初回呼び出しなので、初期値(空文字)が設定される
  const [inputText, setInputText] = useState("");
  const [convertedText, setConvertedTest] = useState("");

  1. handleChangeは内部関数定義だからなにもせずに飛ばす

  1. 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>
  )

  1. Reactが、描画関数を呼んで、HTMLを描画する

これで、初期表示は、中身が空で表示されます。

初期表示
初期表示画面
正確にいうと…

Reactは、厳密に言えば描画関数で直接HTMLを描画しているわけではありません。

Reactはまず仮想DOMに書き込み、そのあと実際のDOMとの差分を描画することで、レンダリングの高速化を図っています。

ただイメージとしては、描画関数でHTMLを描画すると思っておけば問題ないでしょう。

テキスト入力

次に、テキストを入力したときの流れです。

入力内容テキストに、”a”‘ を入力したとします。useStateの動きに注目しましょう。

  • Reactが入力イベントを検知してhandleChangeを呼ぶ
  1. handleChangeの中身が実行される
  const handleChange = (e) => {
    const newText = e.target.value;
    setInputText(newText);
    setConvertedTest(newText.toUpperCase());
  }
  1. set~の呼び出しで、inputTextに”a”、convertedTextに”A”を設定したことを、Reactに通知する

このイベント処理が終わった後、Reactがひと仕事します。

  1. handleChangeが終わると、Reactは、stateが変更された関数コンポーネントを調べる
  2. Reactは、Appが変更されたことを検知し、Appを呼び出す
function App() {

  1. useStateで、変数とそのset関数を取得する。前回のsetで設定した値”a””A”が入る
  const [inputText, setInputText] = useState("");
  const [convertedText, setConvertedTest] = useState("");

  1. 「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>
  )

  1. Reactが、描画関数を呼んで、HTMLを描画する

これで、入力された値で、画面が反映されることになります

更新された画面
更新された画面

フックの役割は「レンダリング」をReactに促すこと

復習しましょう。フックによる大きく動きが変わったのは、ここです。

フックによる効果
  const handleChange = (e) => {
    const newText = e.target.value;
    setInputText(newText);
    setConvertedTest(newText.toUpperCase());
  }
  1. set~の呼び出しで、inputTextに”a”、convertedTextに”A”を設定したことを、Reactに通知する
  2. handleChangeが終わると、Reactは、stateが変更された関数コンポーネントを調べる
  3. Reactは、Appが変更されたことを検知し、Appを呼び出す

そう、useStateフックで提供されるset関数は、ただ変数をセットするだけの関数ではないのです。

このset関数は、Reactに対して再レンダリングを促すために、使うのです。

※描画のことを、一般的に「レンダリング」と呼びます。

useStateでよくある勘違い

useStateが返すsetInputText関数。

set~という名前で勘違いされがちですが

  • setInputTextで”a”を設定する意味は、次の再レンダリングにそれを使う、ことを意味します

なので

  • setInputTextをしたとして、その場で即時にinputTextの中身が変わるわけではありません
  • inputTextが”a”に変わるのは、Reactが関数コンポーネントを再度呼び出した、useStateのタイミングです

動きをちゃんと理解していれば、理解できますね。

Reactの重要な概念「リアクティブ」

Reactのこの独特の仕掛けは、「リアクティブプログラミング」という思想に則っています。

「データ変更」をReactに通知する

先ほどの処理のポイントはuseState。役割はこの通り。

useStateの重要な役割

useStateで返却されるset関数は、

  • 単に変数をセットするためのものではない
  • Reactに「データを変更したから、このコンポーネントは再レンダリングが必要」伝えるためのもの

ユーザーは、関数コンポーネントでレンダリングの定義を書きます。、が、ユーザー側で直接実行はしません。

レンダリングを実行するのは、あくまでReact側

ユーザープログラムは、Reactにレンダリング用の定義(関数コンポーネント)を渡しているだけなのです。

フックで「コンポーネントをReactに結びつける」

Reactフックの大事な役目が、これです。

Reactフックの役割は、Reactに関数コンポーネントを結びつける(フックする)こと

ここで使ったuseStateは、Reactフックのひとつで、一番よく使われます。

フックは他にも、このようなものがあります。

色々なReact Hooks
  • useState
  • useReducer
  • useEffect
  • useContext
  • useCallback
  • useMemo

用途は様々ですが、いずれも、関数コンポーネントの変化をReactに伝えるという役割は同じです。

フックの種類について

フックの種類により、いろいろな役割があります

useState
useReducer
useContext
再レンダリングが必要なことをReactに通知する
useEffect再レンダリングにより発生する副作用を定義する
(再レンダリング時の外部システムへの通知など)
useCallback
useMemo
再レンダリングが不要なことをReactに通知する
(パフォーマンス向上に使う)
フックの種類

役割は違いますが、いずれも再レンダリングに関する制御を受け持っていることが、分かりますね。

データ変更のタイミングで処理を動かす「リアクティブ」

  • データが変更されたら、自動的にReactが描画する
  • なのでユーザーは、データと描画の定義を宣言するだけでよい

この考えが「リアクティブプログラミング」。これまでにはみられなかった、UI開発の新しい概念です。

実は、Reactという名前の由来は、この“Reactive”から来ているわけですね

Reactプログラミングで意識すべきこと3つ

リアクティブな考えでプログラミングするReact。これまでのUIプログラミングとは異なる注意ポイントがあります。

特に、以下の3つは意識しましょう。

Reactプログラミングで意識すべきこと
  • あらゆる状態をフックで管理する
  • 状態の冗長性や矛盾を取り除く
  • コンポーネントを純粋にする

あらゆる状態をフックで管理する

データが変更されたら、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のメリットを存分に活用しましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次