Subterranean Flower

正真正銘のReactだけの不純物なしでReact入門

Author
古都こと
ふよんとえんよぷよぐやま!!!

Reactのチュートリアル、たくさんありますよね。どれも質が高く、どこから手をつければいいかわからなくなっちゃいます。

ですがやはり巷のチュートリアルには面倒な問題もあります。今回は面倒ごとを全部すっ飛ばしてReactでのウェブアプリ作りに入門してみましょう。

Reactを始めるには、まずあれとこれとそれとどれと……

Reactやるには、まずNode.js入れてbabel入れてreact入れてreact-router入れて、ついでにredux入れてreact-redux入れて、redux-saga入れて……

Reactめんどくせえ!!!ってのが正直なところだと思います。はい、私もそう思います。ただ、まあ、色々必要なのも事実なので……。

それでもやっぱり「ReactやるならReactだけやりたい。他はどうでもいい」という気持ちは簡単に捨てられるものではありません。そこで今回はそういう面倒全部無視して、本当にReactだけでウェブアプリ作っちゃいましょう。

この記事の注意点

この記事を読むにあたっていくつか注意点があります。

まず最初に、この記事の内容は現実のプロダクトにそのまま適用できないということ。Reactのみにフォーカスした学習はできますが、その代償として現実的な開発手法も抜け落ちています。まともにアプリを作ろうとすると結局Node.js入れてbabel入れて、となります。まあそのあたりは各自あとで調べてください。

次に、React以外を削ぎ落としたからといってそこまで簡単にはならないということ。最近ではReactも高機能化が進み様々なことが実現できるようになりましたが、裏を返せば今までライブラリで実現していたような複雑性も取り込んでしまっています。よってそこまで簡単になるわけではないので注意です。

そして、HTML/CSS/JavaScriptについてはあらかじめある程度精通していることを前提とします。これらの説明をしていると長くなるためです。Reactのみにフォーカスするため、あらかじめご了承ください。

今回作るもの

独り用SNS「孤独ったー」を作ってみましょう。ユーザはあなた独りで、孤独にツイートします。

入力欄と、ツイートボタンとツイート一覧だけです。シンプル!それではさっそく作っていきましょう。

前準備と初めての起動

開発環境の構築

まず開発環境を用意します。ブラウザ、エディタ、サーバを用意します。

ブラウザはChromeにしましょう。他のブラウザでも動作しますが、現段階ではChromeが最も安定した動作が期待できます。

適当なエディタと動作確認用ローカルウェブサーバが必要です。メモ帳とコマンドプロンプトだけでもいいのですが、基本的にVisual Studio Codeを前提としていきます。

VS Codeは以下のリンクからダウンロードできます。インストールしてください。

https://code.visualstudio.com/

サーバはなんでもよく、pythonとかでもいいのですが、よくわからない人はVS Codeの拡張機能で行きましょう。VS Codeのインストール後、以下のURLからインストールします。ページ内の「Install」を押すとVS Codeの拡張機能インストール画面が開くので、そこでさらに「Install」です。

https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer

ここまでできたらプロジェクトフォルダの準備をします。VS Codeの「ファイル → ワークスペースを開く」から、適当なフォルダを作って開きます。フォルダ名は「react-only-tutorial」などでいいでしょう。

これで前準備は完了です。

Hello React!

まずはReactを動かしてみて、それから説明に入りましょう。

Reactの最も簡単な使用方法は、CDNからライブラリを読み込むだけです。CDNからはreact本体とreact-dom、babelを読み込みます。

まずは以下のようなindex.htmlを作成します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Cache-Control" content="no-cache">
    <title>React Tutorial</title>
    <link rel="stylesheet" href="style.css">

    <!-- React関係のライブラリの読み込み -->
    <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

    <!-- 自分で書くスクリプト -->
    <script type="text/babel" src="main.jsx" defer></script>
  </head>

  <body>
    <!-- Reactの描画対象を準備しておく -->
    <div id="app"></div>
  </body>
</html>

index.htmlでは3つ+1つのスクリプトを読み込んでいます。react本体と、DOM操作を行うreact-dom、それに加えてbabelというものを読み込んでいます。また、「main.jsx」というファイルも読み込んでいます。

なお、これら3つのreact、react-dom、babelのスクリプトファイルは本来は試作用のものであり、製品に採用するものではありません。これらスクリプトファイルを使うのはこの記事だけにしておいて、他のところではちゃんとNode.jsなどを入れてビルドしてください。

次にmain.jsxを作成します。拡張子は「jsx」なので「js」と間違えないように。main.jsxの内容は以下のようにします:

function App() {
  return <div>Hello React!</div>;
}

const target = document.querySelector('#app');
ReactDOM.render(<App/>, target);

ここまでできたら、サーバを立てて、ブラウザでindex.htmlアクセスします。VS Codeの拡張機能を使用しているなら、下の青いバーのところに「Go Live」とあるはずなのでクリックしてください。サーバが起動し、自動的にブラウザで開かれるはずです。

「Hello React!」の文字が表示されたでしょうか?おめでとうございます。あなたはReactの第一歩を踏み出しました!

表示されないという方はブラウザ上でF12キーを押して、コンソールを見てください。赤いエラーが表示されているかもしれません。黄色は無視してかまいません。ちなみにfaviconが404という赤いエラーが出てると思いますが、それは全く関係ないので放置で大丈夫です。他にエラーが出ているのなら、どこかを間違えているので、もう一度index.htmlとmain.jsxを確認してください。

また、ファイルからindex.htmlを直接開くとjsxファイルを読み込めずに失敗します。必ずサーバを立てて、そこにアクセスしてください。

Reactって何なんだろう

Reactとは何か

先ほどの例では画面に「Hello React!」と表示するだけで、Reactの正体はつかめませんでした。Reactとはなんなのでしょう?

公式サイトを見ると、Reactは「A JavaScript library for building user interfaces」と称されています。UIのためのライブラリと理解して良いでしょう。

Reactの主な仕事は、実を言うと「データを画面に表示する」だけです。プログラマはデータを用意し、Reactに表示してもらうのです。「Hello React!」の例は、まさにReactの本質とも言えます。文字列を画面に表示しているわけですから。たったそれだけ?と思うかもしれませんが、たったそれだけです。

さらに「Reactに与えるデータを変えると表示されるデータも変わる」という機能も備えています。あなたがデータを変更してReactに与えれば、変更部分を半自動的に検出し、反応的(reactive)に表示を変えます。

この反応的変化がReactの価値を大きく高めています。例えばTwitterを作ることを考えましょう。多数の投稿が順番に並び、「いいね」や「リツイート」によって状態が変化します。さらにリプライがつくかもしれません。ツイートが消されることも。そうすると画面に「変化」を起こさないといけません。ユーザたちの何らかのアクションを画面に反映しようとすると、非常に多数の変数と状態管理が必要になります。JavaScriptだけでこれを解決しようとすると、複雑に絡んだいわゆるスパゲティコードになりがちです。そういった場合にReactを使えば、この変更を感知し、自動的に画面に反映させることができます。

また、Reactは表示要素のコンポーネント化(部品化)についても長けています。いくつかの表示要素と機能をまとめ、コンポーネントとして利用可能にします。ウェブにもWeb Componentsというコンポーネント化の標準規格がありますが、まだまだ実用的ではないということもあり、Reactが採用される場面が多く存在します。

現代のウェブアプリはリッチな作りであることが多く、ユーザ入力や通信によって表示内容が高速に切り替わっていきます。しかし手作業のコードで全てを書き換えていくのは現実的ではありません。そのような時代において、「画面表示の専門家であるReact」の必要性が高まるのは必然でした。

ただしUI特化であるため、Reactひとつだけで完結するアプリは、世の中にはなかなかありません。最近はReactも高機能化してきたので作れる範囲も広がりはしたのですが、やはり多彩なサードパーティのライブラリに頼らざるを得なくなる部分も出てきます。多くのチュートリアルサイトでよくわからないものを沢山インストールさせられるのはこれが原因です。

Reactはあなたの期待には応えない

Reactの概念を理解するのに苦労する人が多いのは、Reactにあまりにも多くのことを期待しすぎるためです。Reactはウェブアプリの制作を全面的に助けてくれるわけではありません。Reactはただデータを表示するだけです。それ以外のことは、全部プログラマがする必要があります。

「データを表示するだけなんて、ただのJavaScriptでもできるじゃないか」という気持ちが、Reactの理解を邪魔します。それは全く正しい事実で、だれも否定できないからです。

しかし、ウェブアプリ制作の経験者たちはひとつの事実に気づいています。「データを画面に正しく表示するのは、とんでもなく難しい」と。データの表示をコードに起こすと、たとえ熟練者であっても大抵の場合はあまりにもひどいコードが出来上がり、バグは満載になります。そしてReactはそのとんでもなく難しい部分を完璧にフォローしてくれます。

「可能である」と「スマートに実現できる」の間には、高い壁が存在します。JavaScriptのみによるデータの表示は「可能である」ですが、スマートに実現することは、ほぼ不可能です。ですがReactを使えば、データ表示をスマートに実現できます。そこがReactの価値であり、本質でもあります。

Reactを構成する概念

コンポーネントとJSX

ReactではUI表示部品のことをコンポーネントと呼びます。コンポーネントはある程度のHTML要素をまとめたものであることが多く、普通のHTMLのタグと似たようなものと考えていいでしょう。

入れ子にすることもできます。例えば「サイドバー」コンポーネントの中の「メニュー」コンポーネント、メニューコンポーネントの中の「項目」コンポーネント、という具合に作ることができます。

各コンポーネントは独立に「状態」を持つことができ、また外部からの値の受け渡しにも対応しています。これについては後述します。

コンポーネントは最終的にHTML要素として表示されます。Reactが受け取った値などをもとに、具体的なHTML要素として実体化します。

例えば先ほどの例で言えばAppコンポーネントが存在することになります。

function App() {
  return <div>Hello React!</div>;
}

const target = document.querySelector('#app');
ReactDOM.render(<App/>, target);

コンポーネントの定義は、関数またはクラスで行います。上の例で言えば関数によってAppコンポーネントを定義しています。App関数は何も値を受け取らず、単純なdiv要素を返します。より複雑なコンポーネントについては後ほど扱います。

そしていきなり不思議な記法が登場しましたね。JavaScriptなのに<div></div>などとHTMLのような表記をしています。これは実はJavaScriptの拡張で「JSX」と言います。React専用の記法だと思っていてください。そしてもちろんこの「JSX」はReact独自のものなので、このままではブラウザで全く動きません。エラーになります。なのでbabelという変換器を通して普通のJavaScriptに変換します。先ほどscriptタグに記述した中のひとつがbabelですね。

    <!-- React関係のライブラリの読み込み -->
    <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

    <!-- 自分で書くスクリプト -->
    <script type="text/babel" src="main.jsx" defer></script>

main.jsxファイルはそのままでは実行できないので、typeをtext/babelにして、ブラウザが勝手に読み込まないようにしています。そしてbabelがmain.jsxファイルを変換してJavaScriptの形式にしてくれます。

JSXはbabelによってReact.createElementという普通の関数に変換されます。名前の通り要素を作るだけの関数ですね。createElementを直接書くこともできますが、基本的にはJSXを書いて変換して動かすのが主流です。

JSXには普通のHTML要素以外にも、自分で定義したコンポーネントを利用することが可能です。最後の行の<App/>がそれにあたります。

なお、HTMLと違って閉じタグのない要素は<>の中の最後にスラッシュをつける必要があります。<input/>とか<img/>とかです。自作コンポーネントでも中身を記述しないなら同様の書き方をします。

レンダリングと差分検知

ReactはJSXで与えられたコンポーネントを、対象のHTML要素の中に描画(render)します。

const target = document.querySelector('#app');
ReactDOM.render(<App/>, target);

これはidがappとなっている要素の中に、自分で定義したAppコンポーネントを描画しています。id=”app”の要素はあらかじめHTML内に用意しています。

何かデータに変化があるたびにReactは<App/>をレンダリングします。少しでもデータに変化があれば全ての要素に伝播していきHTML要素として描画します。「それってアプリの規模が大きくなるとものすごく重い処理なのでは」と心配の方、大丈夫です。Reactは差分検知システムを搭載しており、実際に書き換えるのは値が変わったコンポーネントだけです。

なお、ここでレンダリングはreact-domというモジュールに切り離されています。HTMLのscriptタグに書いたやつですね。これはReactの活用範囲が広がって対象がHTMLだけとは限らなくなった(スマホアプリも作れるようになった等)ため、HTML関連だけ分離したのです。

もう一度コードを見てみよう

ここまでの知識で、もう一度コードを見てみましょう。

function App() {
  return <div>Hello React!</div>;
}

const target = document.querySelector('#app');
ReactDOM.render(<App/>, target);

まずAppというコンポーネントを関数で定義しています。AppコンポーネントはJSXのdiv要素を返します。そのAppコンポーネントをid=”app”の要素にレンダリングしています。Reactではこのような流れで画面を描画します。

Reactについてまとめ

  • ReactはUI構築ライブラリである
  • Reactは「データの表示」のみを担当する
  • コンポーネントという部品単位で表示を扱う
  • データの更新があった際、自動的に表示を更新する
  • 表示の更新には差分検知を用いており、無駄が少ない

Reactに慣れていく

コンポーネントを作ってみる

先ほど作成したindex.htmlとmain.jsxはそのままにしておきましょう。次は実用的なコンポーネントを作ってみましょう。

独り用SNS「孤独ったー」を作るためには、まずはツイートの表示が必要になります。Tweetコンポーネントを作ってみましょう。

コンポーネントの作り方には2種類あり、シンプルながらも機能制限が多い関数コンポーネントと、機能豊富ながら複雑になりがちなクラスコンポーネントがあります。関数コンポーネントの方がシンプルなので今回はそれで行きます。

Tweet.jsxというファイルを作ります。Tweet.jsxの中には以下のように書きます:

function Tweet(props) {
  return <div>{props.content}</div>;
}

関数コンポーネントは通常の関数として定義します。コンポーネント名は通常は大文字から始まります。これは既存のHTML要素と区別をつけるためでもあります。

関数コンポーネントは引数をひとつ受け取ることができます。コンポーネントが受け取る値はpropsと呼ばれ、これが引数に入ります。例えば<Tweet content=”Hello”/>とするとprops.contentにHelloが入ります。

propsには様々なデータを渡すことができ、それに応じて描画内容を変えることができます。一度コンポーネントを作ればpropsだけを変えて何度も使い回しができるということです。

関数コンポーネントはJSXをreturnします。このreturnした値が実際に表示される部分になります。

JSX中に式を{}で囲むことで、JavaScriptとして評価できます。今回はcontentというpropsの中身を表示するようにしました。{props.content}と書かずにprops.contentと書くとそのままprops.contentという文字が表示されるので、注意してください。

次にindex.htmlをいじってTweet.jsxを読み込むようにしておきます。main.jsxより先に読み込みます。

    <!-- 自分で書くスクリプト -->
    <script type="text/babel" src="Tweet.jsx" defer></script>
    <script type="text/babel" src="main.jsx" defer></script>

そしてこれをmain.jsxから使用してみます。

function App() {
  return (
    <div>
      <Tweet content="Hello" />
      <Tweet content="React!" />
    </div>
  );
}

const target = document.querySelector('#app');
ReactDOM.render(<App/>, target);

Appコンポーネントの中にTweetコンポーネントを記述しています。このときcontentというpropsを渡しています。こうして渡したpropsは関数コンポーネントの引数に入ります。Tweetコンポーネントは受け取ったprops.contentの中身を表示するだけのコンポーネントなので、ここで渡した値がそのまま表示されます。

関数コンポーネントが要素をreturnするときの注意点ですが、要素をひとつしかreturnできません。2つ以上の要素を返したいときは必ず何かの要素(今回はdiv)で囲んで、ひとつにまとめる必要があります。

ちなみにどうしても2つ以上返したい場合は<>と</>を使って囲んでひとつにまとめると<></>は無視されて描画されるようになります。

あとは普通にrenderするだけです。もう一度サーバを立てて、アクセスしてみましょう。

表示されましたね!

Tweetコンポーネントの強化

Tweetコンポーネントはこのままでは役に立ちません。より表示を強化してみましょう。

Tweet.jsxを書き換えます。アイコン、名前、アカウント名、ツイート内容を表示するように変更します。

function Tweet(props) {
  return (
    <div className="tweet">
      <div className="icon-container">{props.icon}</div>
      <div className="body-container">
        <div className="status-display">
          <span className="display-name">{props.displayName}</span>
          <span className="account-name">@{props.accountName}</span>
        </div>
        <div className="content">{props.content}</div>
      </div>
    </div>
  );
}

関数コンポーネントはJSX要素をひとつ返すので、ひとつのdivの中に全部いれて返しています。divの中は大きく分けて2つになっていて、アイコン部分とコンテンツ部分です。コンテンツ部分はさらに細かく分かれています。あとは必要な値をpropsから取り出しています。

ここでひとつ注意です。JSXは基本的にHTMLと同じですが、クラス名を指定するときはclassではなくclassNameを使用します。これはJavaScriptではclassは特別な意味を持つため、名前が被らないようになっています。

見た目も整えましょう。style.cssというファイルを作ります。これはすでにHTMLから読み込むのを指定しているので、作成するだけで勝手に読み込まれるはずです。内容は以下のようにします:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background-color: rgb(240, 240, 255)
}

.tweet {
  display: flex;
  background-color: white;
}

.tweet:not(:last-child) {
  border-bottom: 1px solid rgb(200, 200, 200);
}

.icon-container {
  width: 1.5em;
  font-size: 3em;
  padding: 0.2em;
}

.body-container {
  flex: 1;
  padding: 0.5em;
}

.account-name {
  color: gray;
}

あとはmain.jsxからTweetを使うだけです。propsがかなり増えたのに気をつけてくださいね。

function App() {
  return (
    <div>
      <Tweet
        icon="🌽"
        displayName="もろこし太郎"
        accountName="morokoshi"
        content="今日も1日もろこしがうまい"
      />
      <Tweet
        icon="🦐"
        displayName="エビデンス"
        accountName="evidence"
        content="かにみそたべたい"
      />
    </div>
  );
}

const target = document.querySelector('#app');
ReactDOM.render(<App/>, target);

ツイートを2つ追加してみました。これで表示されるか試してみましょう。

なかなかいい感じですね!Reactではこんな感じでコンポーネントを作ってpropsを与えて描画します。

コンポーネントの状態とHooks

いいね機能と状態とHooks

次はいいね機能をつけてみましょう。といってもクリックしたら表示を切り替えるだけですが。

いいね機能をつけるためには「コンポーネントに”状態”を持たせる」という仕組みが必要になります。しかし関数は一般的に状態を持てません(一度実行すると関数が終わるため)。どうやって解決すればいいでしょうか。

一番愚直な方法は変数をグローバルに持ち、関数からそれを参照する方法です。この方法は最もシンプルですが、問題は「ダサい」ことです。また、グローバルに変数を展開するので、グローバルがどんどん汚染されていきます。

ひとつはクラスコンポーネントを使うことです。クラスコンポーネントは普通のクラスなので、関数コンポーネントと違い、変数を維持することができます。デメリットは変数の保持のためだけにクラスを採用すると問題を複雑化させやすいところです。

もうひとつはReactの機能を使うことです。ReactにはHooksという機能があり、変数をコンポーネントごとにReact側で管理してくれます。コンポーネントごとに専用の変数ストレージができるイメージです。今回はこれを使いましょう。

Hooksの状態管理機能を使うのは簡単で、関数の中でReact.useStateを呼び出すだけです。

const [value, setValue] = React.useState('デフォルト値');

useStateはデフォルト値を与えると、前にコンポーネント関数を呼び出した時の値(なければデフォルト値)と、値を更新するための関数を返します。

値を更新するときは、先ほど入手した更新関数で値を更新します。

const [value, setValue] = React.useState('デフォルト値');
setValue('新しい値!');

複数の状態を扱うときは複数回呼び出します。

const [value1, setValue1] = React.useState('デフォルト値');
const [value2, setValue2] = React.useState('デフォルト値');

これだけです。あとはReactが値を管理してくれるので、関数が終わっても値が保持され続けて、次に関数が呼び出されたときに前の値を入手してから処理ができます。

ただし制限があります。useStateは状態に名前等をつけずに、番号だけを振っています。つまり「0番目の状態、1番目の状態」などです。それをuseStateを呼び出すたびに順番に取り出しているだけです。つまり、useStateを呼び出す順番や回数が変わると、手に入るデータがずれます。よくやりがちなのがif文の中でuseStateして、useStateを呼び出す回数が変わってしまってデータがずれることです。

難しいことを考えたくなければ、関数の先頭にuseStateを全部ベタ書きしましょう。なんの問題もなく動きます。

いいね表示

先ほどのuseStateを使って、Tweetコンポーネントが状態を保持できるようにしましょう。「いいね」したかどうかの値だけなので、useStateを1回呼び出して状態をひとつ手に入れます。デフォルト値はfalse(いいねされていない)にしておきます。そしてそれに応じてハートを表示します。

function Tweet(props) {
  // Reactからこのコンポーネントの
  // like値と、likeの値をセットするための関数を取り出す
  // デフォルト値はfalseにする
  const [liked, setLike] = React.useState(false);

  return (
    <div className="tweet">
      <div className="icon-container">{props.icon}</div>
      <div className="body-container">
        <div className="status-display">
          <span className="display-name">{props.displayName}</span>
          <span className="account-name">@{props.accountName}</span>
        </div>
        <div className="content">{props.content}</div>
        <div className="status-action">
          <span>{liked ? '❤️' : '♡'}</span>
        </div>
      </div>
    </div>
  );
}

これでツイート下部に白いハートが表示されるはずです。しかしまだクリックして切り替え機能をつけていないので、何も起きません。

クリックしたらxxする

あとはonClickで……いや、少し待ってください!ここにReactのちょっとした罠があります。普通にやると以下のようにコールバック関数を用意してonClickに設定すると思います:

const [liked, setLike] = React.useState(false);
const toggleLike = () => setLike(!liked);

このtoggleLikeをonClickに設定するとまずいことが起こります。このtoggleLikeは関数を実行するたびに新しく作られます。そしてonClickに設定すると、毎回新しい関数がセットさせることになります。Reactはこれを「新しい関数がセットされたのだから、コンポーネントの状態が更新されている」と認識して、毎回表示を更新するようになります。

べつにバグが起きたりはせず普通に動くのですが、Reactの「本当に変更された部分だけ表示を更新する」という利点を活かせません。また、こういう無駄があるとプログラムの規模が大きくなってくるとかなり重くなります。無駄な更新を避けるためにも、コールバック関数もどこかに保存して使い回すほうがベターです。

そのための機能としてuseCallbackというのがあります。useCallbackはコールバック関数を保存しておくための機能です。以下のように使います:

const [liked, setLike] = React.useState(false);
const toggleLike = React.useCallback(() => setLike(!liked));

これでコールバック関数を保存してくれ……ません!これでも毎回コールバック関数が作成されます。ええぇ……。実はuseCallbackには第2引数が存在し、そこに配列を渡さないと保存してくれないのです。例えば次のような配列を渡します:

const [liked, setLike] = React.useState(false);
const toggleLike = React.useCallback(() => setLike(!liked), [setLike]);

これでキャッシュしてくれます。第2引数の配列は「この配列の中の値が変わったらまた新しく関数を作ってね!」という意味になります。今回はsetLike関数が変わるたびにコールバック関数を作り直してもらいます。setLike関数さえ変わらなければ最初に作ったコールバック関数を使いまわします。

すると新しい問題が起きます。こうするとコールバック関数は最初の1回しか作られないため、likedの値も最初の1回目を参照して常にfalseです。つまり!likedは常にtrueです。切り替えできてないです。

ここで、実はuseStateの更新関数には関数を渡せます。ここで渡す関数には、現在の値が引数として渡されます。そして値を返すとその値を新しい値としてセットしてくれます。つまり以下のように書けば、やっと動きます:

const [liked, setLike] = React.useState(false);
const toggleLike = React.useCallback(() => setLike((prev) => !prev), [setLike]);

つ、疲れた……。これをTweet.jsxに適用してみます。ハートを表示するspan要素のonClickにtoggleLike関数を渡すのを忘れずに。

function Tweet(props) {
  // Reactからこのコンポーネントの
  // like値と、likeの値をセットするための関数を取り出す
  // デフォルト値はfalseにする
  const [liked, setLike] = React.useState(false);
  const toggleLike = React.useCallback(() => setLike((prev) => !prev), [setLike]);

  return (
    <div className="tweet">
      <div className="icon-container">{props.icon}</div>
      <div className="body-container">
        <div className="status-display">
          <span className="display-name">{props.displayName}</span>
          <span className="account-name">@{props.accountName}</span>
        </div>
        <div className="content">{props.content}</div>
        <div className="status-action">
          <span onClick={toggleLike}>{liked ? '❤️' : '♡'}</span>
        </div>
      </div>
    </div>
  );
}

コードとして評価したい場合は{}で囲む必要があるので、関数を渡すときはonClick={toggleLike}のように書きます。

これで一度結果を見てみましょう。

ハートをクリックすると切り替わるはずです。

Reactではこんな風にコンポーネントの状態を管理します。

動的にデータを更新する

コンポーネントリストの表示

今まではTweetコンポーネントを手書きしていましたが、普通は配列でツイートリストが渡ってきて、それに応じてTweetコンポーネントが表示されるはずです。これを実現してみましょう。

Timeline.jsxという新しいファイルを作ります。

function Timeline(props) {
  // propsからTweetのリストを作る
  const tweetList = props.tweets.map((tw) => (
    <Tweet
      key={tw.id}
      icon={tw.icon}
      displayName={tw.displayName}
      accountName={tw.accountName}
      content={tw.content}
    />
  ));

  // 表示する
  return <div className="timeline">{tweetList}</div>;
}

Timelineコンポーネントではpropsとしてtweetsという配列を受け取り、それぞれをTweetコンポーネントに変換しています。これでTweetコンポーネントの配列ができます。

各Tweetにはkeyという特殊なpropsを指定しています。コンポーネントの配列を生成するときには必ずkeyをつけます。keyにはユニークな値を指定します。このkeyはReactが「どの要素がどこにいるのか」を把握するために使われます。省略したりキーが重複していたりするといろいろなところで不都合が発生します。例えばuseStateが全く違うコンポーネントの値を取ってきてしまったりします。

コンポーネント配列を表示するには、ただ単にJSXの中で{}で囲むだけです。

次にindex.htmlに久々にスクリプトを追加します。今さっき書いたTimeline.jsxです。

    <!-- 自分で書くスクリプト -->
    <script type="text/babel" src="Tweet.jsx" defer></script>
    <script type="text/babel" src="Timeline.jsx" defer></script>
    <script type="text/babel" src="main.jsx" defer></script>

これでTimelineコンポーネントが使えるようになります。main.jsxの中のAppコンポーネントを変更してみましょう。

function App() {
  const tweets = [
    {
      id: 0,
      icon: '🌽',
      displayName: 'もろこし太郎',
      accountName: 'morokoshi',
      content: '今日も1日もろこしがうまい'
    },
    {
      id: 1,
      icon: '🦐',
      displayName: 'エビデンス',
      accountName: 'evidence',
      content: 'かにみそたべたい'
    }
  ];

  return (
    <div>
      <Timeline tweets={tweets}/>
    </div>
  );
}

const target = document.querySelector('#app');
ReactDOM.render(<App/>, target);

ツイートを配列化し、Timelineコンポーネントに渡すように変更しています。これで表示は変わらず、配列からの描画ができたと思います。一度動かして確認してみてください。

コンポーネントの動的追加

一般に、コンポーネントは初めから決まりきったものではなく、場合によって動的に変化します。例えばTweetコンポーネントは誰かがツイートするたびに増えます。

まずツイートを入力して加えるためのコンポーネントTweetInputを作成します。TweetInput.jsxを作成してください。内容は以下のようにします:

function TweetInput(props) {
  // 要素にアクセスするための参照を取得
  // ref.currentのデフォルト値はnullにしておく
  // 参照したい要素にJSXの方でこのrefをセットする
  const textareaRef = React.useRef(null);

  // Tweetボタンクリック時のコールバック関数を作って保存
  // textareaRef.currentかprops.addTweetが更新されたら
  // コールバック関数を作り直し
  const sendTweet = React.useCallback(() => {
    // refのcurrentに入っている値がDOM
    // 初期値はnull(上で指定した)で、
    // 実際にHTML側に描画されると値が入ります
    if(textareaRef.current) {
      props.addTweet({
        id: new Date().getTime(), // IDはユニークな値にする
        icon: '☠️', // このあたりの値は好きにしてください
        displayName: 'ミスター死',
        accountName: 'mrdeath',
        content: textareaRef.current.value
      });
    }
  }, [textareaRef.current, props.addTweet]);

  return (
    <div>
      <div><textarea className="tweet-textarea" ref={textareaRef}></textarea></div>
      <div><button onClick={sendTweet} className="send-tweet">Tweet</button></div>
    </div>
  );
}

useRefというHooksが出てきました(HooksはすべてuseXXXという名前になります)。useRefはJSXで書かれた要素にスクリプトからアクセスするために必要になります。useStateと同じく、呼び出す順番と回数は常に一定である必要があります。これで得られたrefをJSXの方に貼り付けます。今回はtextarea要素にrefを追加しています。

refはref.currentで実際の要素にアクセスできます。今回はデフォルト値をnullとしたので、初めはref.currentにはnullしか入っていません。

次にsendTweetコールバック関数です。これはツイートボタンのonClickに割り当てるためのコールバック関数です。もちろんuseCallbackを使います。コールバックの更新条件はtextareaRef.currentかprops.addTweetが更新されたときとしました。更新条件は第2引数に配列で渡します。

ref経由でtextareaの存在をチェックし、ref経由でtextarea要素の値を見ます。textareaRef.currentに値が入っていれば存在しています。

propsはなんでも渡せるので、今回は関数をpropsとして渡してもらっています。addTweetという関数を親から受け取ります。この関数を経由してツイートを追加します。あとはツイートボタンのonClickにsendTweetを割り当てるのを忘れずに。

index.htmlにこのTweetInput.jsxを追加しておきます:

    <!-- 自分で書くスクリプト -->
    <script type="text/babel" src="Tweet.jsx" defer></script>
    <script type="text/babel" src="Timeline.jsx" defer></script>
    <script type="text/babel" src="TweetInput.jsx" defer></script>
    <script type="text/babel" src="main.jsx" defer></script>

main.jsxのAppコンポーネントでaddTweet関数を作り、TweetInputコンポーネントに渡します:

function App() {
  // useStateでツイート配列を取得する
  // 初期値は今まで通り
  const [tweets, setTweets] = React.useState([
    {
      id: 0,
      icon: '🌽',
      displayName: 'もろこし太郎',
      accountName: 'morokoshi',
      content: '今日も1日もろこしがうまい'
    },
    {
      id: 1,
      icon: '🦐',
      displayName: 'エビデンス',
      accountName: 'evidence',
      content: 'かにみそたべたい'
    }
  ]);

  // addTweet関数はuseCallbackで作成する
  // これも毎回作成していると重くなるので
  const addTweet = React.useCallback((tweet) => setTweets((prev) => [tweet, ...prev]), [setTweets]);

  return (
    <div>
      <TweetInput addTweet={addTweet}/>
      <Timeline tweets={tweets}/>
    </div>
  );
}

const target = document.querySelector('#app');
ReactDOM.render(<App/>, target);

まず、ツイート一覧の管理をuseStateに切り替えました。固定ではなく、どんどん変化していくからです。

addTweet関数はツイートデータが渡されるとツイートリストの先頭にツイートを追加する動きをします。これをuseCallbackを使ってキャッシュします。あとはTweetInputコンポーネントにaddTweetを渡すだけです。

style.cssにもスタイルをちょっと追加します。

.tweet-textarea {
  font-size: 1em;
  width: 100%;
  height: 5em;
  padding: 0.8em;
  resize: vertical;
}

.send-tweet {
  font-size: 1em;
  padding: 0.5em 1em;
}

これで実行してみましょう。

入力欄が現れましたね。さっそくツイートしてみましょう。

できました!!完成です!!!

ここから先のこと

今回はReactだけを用いて簡単なアプリを作りました。シンプルゆえに魅力が伝わりにくい部分があったとは思いますが、Reactの実力の片鱗だけでも味わってくれていれば、と思います。

もちろん、Reactにはまだまだできることがたくさんあります。Hooksはもっといろんな種類があって、様々な挙動にフックすることができます。React17で導入予定のSuspenseというのを使えば非同期処理をReactだけで簡単に書くこともできます。今回は使いませんでしたが、より複雑なことが実現できるクラスコンポーネントというものもあります。

また、より現実的な開発になってくると今回の方法では実行があまりにも遅すぎるので、Node.jsをいれて、webpackをいれて……と、よくあるチュートリアルのようになってきます。適切なビルド方法を学ぶのもReactでのウェブアプリ開発には必須になってきます。

そして「非同期処理はどう書くべきか?」「画面遷移はどうやって実現すれば?」などの問題も発生してきます。こういった諸問題は有志のライブラリが解決してくれることが多く、それらの使い方を学ぶことになるでしょう。

Reactだけを学んでも実現できることはそんなに多くなく、実際は周辺技術とともに活用していくことになります。どれも複雑な機構をしていますが、ひとつずつ学んでいけば、身に付けるのはそんなに難しくないはずです。

それでは、良いウェブアプリ開発者を目指して頑張ってください。