【ベテランほど悩む】あなたのReactコードはなぜ動かないのか?

react-behavior

React。WebフロントエンドのUI開発で使われる、JavaScriptライブラリ。

そのシンプルさや簡潔さで、近年は人気が非常に高まっています。

そんなReactですが、いざ作ってみると、全然思った通りに動かない、ということが起こります。

特に、これまでUI開発をこなしていたベテランほど、泥沼にハマったり

Reactにある、これまでのUI開発とは異なる思想が、けっこう罠になるのです。正しい理解を身に着けて、適切なReactコードを書きましょう。

目次

こんなReactコードは動かない

まずは、UI開発経験者がやりがちな、「動かないReactコード」の例です。

サンプル作ってみたけれど…

Reactで開発することになったあなた。

チュートリアルでさくっと勉強。関数コンポーネント?JSX?フック?ステート?…まあやってみれば分かるでしょう。

とりあえず簡単なサンプル書いてみよう。

入力内容に書いたテキストを、大文字にして、変換結果に表示する、こんなイメージ。

シンプルな画面

inputTextonChangeイベントで出力しよう。こんな感じかな?

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が何をしているのかを理解する

先ほどのコード、「っぽい」とこが本当は何なのか、見ていきます。

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を描画する

ということをしているのです。

処理の流れを追ってみよう

ここまでを踏まえて、どういう動きをしているのか、流れを追ってみましょう。

初期表示

まず初期表示の流れです。

  1. Reactが、関数コンポーネントAppを呼び出す
function App() {
  1. inputText,convertedTextに空文字が設定される
  let inputText = "";
  let convertedText = "";
  1. handleChangeは内部関数定義だからなにもしない
  2. 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を描画する

これで、空文字の画面が表示されます。問題ないですね。

テキスト入力

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

  1. ReactがhandleChangeを呼ぶ
  2. 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で管理するための機能です

うーん分からん。

実際、フックは実際に動かして体験しないと、ピンとこないところあります。

動きを確認して、感じを掴みましょう。

処理の流れを追ってみよう

それでは、フックで書き換えた処理の、流れを追ってみましょう。

初期表示

初期表示の流れです。

  1. Reactが、関数コンポーネントAppを呼び出す
function App() {
  1. useStateで、変数とそのset関数を取得する。初回呼び出しなので、初期値(空文字)が設定される
  const [inputText, setInputText] = useState("");
  const [convertedText, setConvertedTest] = useState("");
  1. handleChangeは内部関数定義でなにもしない
  2. 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がhandleChangeを呼ぶ
  • handleChangeの中身が実行される
  const handleChange = (e) => {
    const newText = e.target.value;
    setInputText(newText);
    setConvertedTest(newText.toUpperCase());
  }
  1. set~の呼び出しで、inputTextconvertedTextが変更されたことを、Reactに通知する
  2. handleChangeが終わると、Reactは、stateが変更された関数コンポーネントを調べる
  3. Reactは、Appが変更されたことを検知し、Appを呼び出す
function App() {
  1. useStateで、変数とそのset関数を取得する。2回め呼び出しなので、前回のsetで設定した値が入る
  const [inputText, setInputText] = useState("");
  const [convertedText, setConvertedTest] = useState("");
  1. 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>
  )
  1. Reactが、描画関数を呼んで、HTMLを描画する

今回は、無事にAppが呼ばれ、入力された値で画面が反映されました

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

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

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

先ほどの処理のポイントはuseState。これが持つ役割は、この通り。

useStateの重要な役割

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

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

ユーザーは、関数コンポーネントで描画の定義を書きます。しかし、それをユーザー側が直接呼ぶことはありません。

描画をするのは、あくまでReact側です。ユーザープログラムは、Reactにその定義を渡しているだけなのです。

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

このuseStateは、React Hooksと呼ばれるReactの仕掛けです。

フックは他にも色々あります。よく使われるのはuseStateですが、他にもこのようなものがあります。

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

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

Reactに、自分が作った関数コンポーネントを結びつける(フックする)。それがReact Hooksの役割です。

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

データが変更されたら、自動的にReactが描画する。

なのでユーザーは、データと描画の定義を宣言するだけでよい。

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

実は、Reactの名前の由来は、この“Reactive”から来ているのです。

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

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

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

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

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

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

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

コメント

コメントする

CAPTCHA


目次