POI
React

poiなら簡単💪🏻💪🏻💪🏻今度こそ失敗しないReact入門🎉🎉🎉【写経用】【設定不要】

この記事を読んで手を動かせば、環境構築などの本質的でないことにつまずかずに、React/JSXの基本的な書き方を身につけられます。

なぜなら、

  • poiという環境設定不要のビルドツールを使う1
  • コピペすれば動く完全なサンプルコードを用意してある
  • 動作時のスクショを貼ってあるので、写経したプログラムが正しいのかわかる

からです。
身につけるためには自分の手で動かすことが大事なので、写経しながら、自分なりに改造などしつつ遊んでみてください。想定所用時間は、2〜3時間程度です。

なお、みなさんは、すでに何らかの理由でReactを使いたいという動機があると想定しているため、Reactの魅力については語りません。

環境設定

Node.jsのダウンロードとインストールが必要です。
こちら からダウンロードしてください。

Node.jsがインストールできたら、プロジェクトを作成します。

# ディレクトリ作成
mkdir react-training

# 作成したディレクトリに移動
cd react-training

# プロジェクトの初期化
npm init --yes

# プロジェクトに依存ライブラリを追加
npm install --save poi react react-dom

以上で準備は完了です。あとは、ファイルにJavaScriptのコードを書いて、

npx poi --jsx react index.jsx

を実行すれば、ファイルのコンパイルができます。
コンパイルがうまくいったら、 ブラウザから http://localhost:4000 にアクセスすると表示が確認できるはずです。

↑の例では、index.jsx というファイル名にしていますが、ファイル名は何でもかまいません。
ReactのJSX記法を含むJavaScriptは、.jsxにするという慣習があります(拡張子は動作に影響しないので、.jsでも問題ありません)

また、作成したプログラムを実際に公開する場合は、

npx poi build --jsx react index.jsx

のようにbuildコマンドを使います。./dist以下にコンパイルされたファイルが生成されるので、それらをサーバーに配置してください。なお、ローカルファイルとしてindex.htmlを開いても動かないので注意してください。HTTPサーバー経由で開く必要があります。

☝️ワンポイント

コンパイル時のエラー(構文ミスなど)は、画面上にエラーが表示されるのでわかりやすいのですが、ランタイムエラーが起きると、画面が真っ白になるなど、一見何が起きているのかわからないことがあります。

ある種のランタイムエラーは、JavaScriptコンソールに表示されるので、そちらも合わせて確認するようにしてみてください。Chromeの場合、View -> Developer -> JavaScript Consoleで表示できます。

ソースコード

以下の全サンプルを含むリポジトリを

https://github.com/tai2/poi-react-intro

に用意したので、めんどうな方はこちらを活用してください。

Lesson 1: Hello, World

まずはHello, Worldからです。

import React from 'react'
import ReactDOM from 'react-dom'

ReactDOM.render(<p>Hello, World!</p>, document.getElementById('app'))

先頭で、Reactを使うために必要な2つのモジュールをインポートしています。
次に、インポートした関数を使って、文字を描画しています。render関数は以下のような2つのパラメータを取ります。

ReactDOM.render(*Reactコンポーネント*, *出力先のDOMノード*)

Reactでのコーディングは、JSXを使って見た目を記述していきます。
JSXというのは、Reactのために開発された独自のJavaScript拡張で、これによって、コード内にほとんどHTMLのようなタグを直接埋め込めます。

HTML(DOMノード)はどこで定義されているんだと不思議に思われるかもしれませんが、これはpoiがよしなに定義してくれます。

☝️ワンポイント

JSXには、閉じタグが必須だったり、classをclassNameと書かなければならなかったりと、本物のHTMLと違う部分がいくつかあります。詳細はこちらにまとまってます。

実行結果

hello-world.png

Lesson 2: コンポーネントの作成

Reactでは、コンポーネントと呼ばれるパーツを自分で定義して、それらを組み合わせることで見た目を定義していきます。Reactにおけるコンポーネントとは、JSXを返す関数です。

import React from 'react'
import ReactDOM from 'react-dom'

// HelloWorldコンポーネント
function HelloWorld() {
  return <p>Hello, World!</p>
}

ReactDOM.render(<HelloWorld />, document.getElementById('app'))

☝️ワンポイント

コンポーネントの名前はキャメルケースでないとコンパイルが通りません

実行結果

hello-world.png

Lesson 3: パラメータを渡す

先のコンポーネントでは、常に同じ文字列を返すだけなので、なにもおもしろくありません。
Reactコンポーネントは、propsと呼ばれる唯一の引数を受け取ります。

import React from 'react'
import ReactDOM from 'react-dom'

function HelloMessage(props) {
  return <p>Hello, {props.target}</p>
}

ReactDOM.render(
  <HelloMessage target="React!" />,
  document.getElementById('app')
)

コンポーネントの属性として指定したキーと値が、propsとしてコンポーネントに渡されます。
上述のコードでは、

{ target: 'React!' }

というオブジェクトをHelloMessageコンポーネントに渡しています。

また、JSX内の {} で囲まれた部分(↑の例だとprops.target)は、JavaScriptの式を書くことができます。

実行結果

passing-props.png

☝️ワンポイント

<MyCheckbox checked />

のように値を指定せずに属性を記述すると、値としてtrueが指定されたことになります。

Lesson 4: 子要素を渡す

propsの中には、特別な意味を持ったプロパティーがあります。
子要素としてタグの内部に記述された要素を表す、props.childrenです。

import React from 'react'
import ReactDOM from 'react-dom'

function WrappingBlock(props) {
  const style = {
    padding: '1rem',
    background: 'linear-gradient(#e66465, #9198e5)',
  }
  return <div style={style}>{props.children}</div>
}

ReactDOM.render(
  <WrappingBlock>
    <p>囲まれたテキスト</p>
  </WrappingBlock>,
  document.getElementById('app')
)

WrappingBlock の開きタグと閉じタグで囲まれた中身(<p>囲まれたテキスト</p>の部分)が、props.childrenとしてコンポーネントに渡されます。

☝️ワンポイント

style属性にCSSのプロパティーを含むオブジェクトを渡すことで直接スタイルを指定できます。

実行結果

passing-children.png

Lesson 5: CSSのクラスを指定する

最近のReactビルド環境では、jsコード以外の一般的なファイルもimportで取り込める機能を持っていることが多いです。スタイルシートもimportで取り込めます。

poiは、.module.css という拡張子を持ったファイルを、CSSモジュールとして扱います。CSSモジュールは、クラスの有効範囲をファイル内(ローカル)に限定してくれるため、CSSの運用がかなり簡単かつ安全になります。

CSSモジュールをimportすると、クラス名をキーに持つオブジェクトが得られる(下記コードのstyles)ので、そのプロパティーをclassNameに指定します。

もちろん、通常のグローバルな有効範囲を持つクラスも使用できます。グローバルなスタイルシートは、一度importすれば、すべてのコンポーネントから利用できるようになります。

import React from 'react'
import ReactDOM from 'react-dom'
import './global.css'
import styles from './App.module.css'

function App() {
  return (
    <div className={styles.root}>
      <p className="u-underlineText">グローバルなクラスの指定</p>
      <p className={styles.root_text}>ローカルなクラスの指定</p>
      <p className={`u-underlineText ${styles.root_text}`}>
        グローバルとローカル両方の指定
      </p>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('app'))
/* global.css */

.u-underlineText {
  text-decoration: underline;
}
/* App.module.css */

.root {
  background: lightgray;
  font-family: sans-serif;
}

.root_text {
  font-weight: bold;
}

なお、SASSを使いたい場合は、以下のコマンドで追加のパッケージをインストールする必要があります。

npm install --save-dev  node-sass sass-loader

☝️ワンポイント

テンプレートリテラルを利用すると比較的簡潔にclassNameを記述できます。

実行結果

stylesheet.png

Lesson 6: イベントを処理する

ここまでで、コンポーネントにパラメータを与えて表示を変化させることはできましたが、インタラクティブな要素はまったくありませんでした。アプリをインタラクティブにするには、イベントハンドラーを使います。

通常のJavaScriptでは、addEventListenerや、onclick, onchangeといった属性を使って、DOMオブジェクトにイベントハンドラーを設定しました。これと同じように、JSXでは、onClick, onChangeといったプロパティーに関数を渡します。

イベントハンドラーのコールバック関数には、eventオブジェクトが渡されます。これは、通常のDOMイベントハンドラーに渡されるeventオブジェクトと同じように使えます。

以下のサンプルコードでは、イベントハンドラーにアロー関数を直接渡しています。
アロー関数はES2015で加えられた関数を短く書ける記法です。

function foo(event) {
  console.log(event)
}

// ↑の関数をアロー関数で書くとこうなる
const foo = (event) => {
  console.log(event)
}

// 関数の本体が1ステートメントの場合は{}を省略できる
const foo = (event) => console.log(event)

// 引数が1つの場合は、()を省略できる
const foo = event => console.log(event)

上記の関数定義は、どれも(ほぼ)同じ意味です。

import React from 'react'
import ReactDOM from 'react-dom'

function App() {
  return (
    <div>
      <button onClick={() => console.log('button clicked.')}>ボタン</button>

      <hr />

      <label>
        テキストボックス
        <input
          type="text"
          onChange={event =>
            console.log('text changed. value =', event.target.value)
          }
        />
      </label>

      <hr />

      <label>
        セレクトボックス
        <select
          onChange={event =>
            console.log('select changed. value =', event.target.value)
          }
        >
          <option value="A">選択肢A</option>
          <option value="B">選択肢B</option>
          <option value="C">選択肢C</option>
        </select>
      </label>

      <hr />

      <label>
        ラジオボタンA
        <input
          type="radio"
          name="radioButton"
          value="A"
          onChange={event =>
            console.log('radio button clicked. value =', event.target.value)
          }
        />
      </label>
      <label>
        ラジオボタンB
        <input
          type="radio"
          name="radioButton"
          value="B"
          onChange={event =>
            console.log('radio button clicked. value =', event.target.value)
          }
        />
      </label>
      <label>
        ラジオボタンC
        <input
          type="radio"
          name="radioButton"
          value="C"
          onChange={event =>
            console.log('radio button clicked. value =', event.target.value)
          }
        />
      </label>

      <hr />

      <label>
        チェックボックス
        <input
          type="checkbox"
          onChange={event =>
            console.log('checkbox changed. value =', event.target.checked)
          }
        />
      </label>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('app'))

実行結果

event-handling.png

☝️ワンポイント

JSXはHTMLと違い、タグとテキストが改行で区切られている場合に、その空白が除去されてタグとテキストが隣接している形に変換されます。つまり、

あるテキストと
<span>要素</span>が改行で区切られている

これは、HTMLだと<span>の前に空白文字がひとつ残りますが、JSXでは、

あるテキストと<span>要素</span>が改行で区切られている

このように変換されます。

Lessen 7: コンポーネントに状態を保持する

イベントハンドリングで、ユーザー操作に応じて処理を走らせることができるようになったので、今度は、それに応じてUIを変化させます。

そのためにはstateを使います。stateは、コンポーネント内にオブジェクトとして状態を保持しておき、その状態を元にUIを構築するための仕組みです。setState関数でstateを変化させると、即座にUIに反映されます。

コンポーネントに状態を保持させるためには、さきほどまでの関数によるコンポーネント定義ではなく、Componentを継承させるクラスベースのコンポーネント定義を使う必要があります。

ところで、下記コードではコンポーネントの引数がpropsになっていません。これは、ES2015で導入された分割代入という機能を使っているたです。分割代入では、オブジェクトや配列の要素を個別の変数に分解できます。

const {a, b} = {a: 1, b: 2}
// a === 1 && b === 2 になる

これは、関数の引数宣言でも使えるので、つまり、

const props = { name: 'aaa', address: 'bbb' }

f(props)

function f({ name, address }) {
  // name === 'aaa' && address === 'bbb'  
}

こうなるのです。

// クラスベースのコンポーネント定義を使うためにComponentクラスをimportする。
import React, { Component } from 'react'
import ReactDOM from 'react-dom'

// ユーザー入力を受けるための氏名・住所フォーム
function PersonForm({ onNameChange, onAddressChange }) {
  return (
    <form>
      <label>
        氏名: <input type="text" onChange={onNameChange} />
      </label>
      <label>
        住所: <input type="text" onChange={onAddressChange} />
      </label>
    </form>
  )
}

// 氏名と住所を表示するためのコンポーネント
function PersonInfo({ name, address }) {
  const tableStyle = {
    borderCollapse: 'collapse',
  }
  const cellStyle = {
    border: 'solid black 1px',
    height: '1rem',
    padding: '0.3rem',
  }
  return (
    <table style={tableStyle}>
      <thead>
        <tr>
          <th style={cellStyle}>氏名</th>
          <th style={cellStyle}>住所</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td style={cellStyle}>{name}</td>
          <td style={cellStyle}>{address}</td>
        </tr>
      </tbody>
    </table>
  )
}

// クラスベースのコンポーネント定義。Appコンポーネントは、Componentクラスを継承する。
class App extends Component {

  // 関数によるコンポーネント定義では、propsを関数の引数として受け取っていたが、
  // クラスベースの場合は、コンストラクタの引数として受け取る。
  constructor(props) {
    // 受け取ったpropsは親クラスのコンストラクタにそのまま渡す
    super(props)

    // stateの初期値を定義する。stateはオブジェクトで表現される。
    this.state = {
      name: '',
      address: '',
    }
  }

  // イベントハンドラでユーザー入力を受けたら、その値を使ってstateを更新する
  // stateを更新するためには、直接値を代入してはいけない。必ずthis.setStateで更新する必要がある。
  handleNameChange = ev => {
    this.setState({ name: ev.target.value })
  }
  handleAddressChange = ev => {
    this.setState({ address: ev.target.value })
  }

  // stateの値を元にPersonInfoをレンダリングする(this.state.name, this.state.address)
  render() {
    return (
      <div>
        <PersonForm
          onNameChange={this.handleNameChange}
          onAddressChange={this.handleAddressChange}
        />
        <hr />
        <PersonInfo name={this.state.name} address={this.state.address} />
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('app'))

☝️ワンポイント

extends Componentとする代わりに、extends PureComponentとすると、そのコンポーネントはパフォーマンス的な最適化の施されたコンポーネントになります。この場合、次のような順番で処理が進みます:

  1. 新旧のpropsとstateを比較して、違いが検出されたら次に進む(されなければスキップ)
  2. 新旧の(内部的な)DOM構造を比較して、違いが検出されたら次に進む(されなければスキップ)
  3. 実際のDOM構造を変更する

また、後述のreact-reduxのconnectでラップされたコンポーネントでも、これと似たような最適化が行われます。

ところが、イベントハンドラとして、Lesson 6のように匿名関数を渡すと、毎回新しい関数のインスタンスが生成されてしまい、意図としては同じ関数を渡しているつもりでも、1のステップで違うと判定されてDOM構造の比較が発生してしまいます(ツリーが大きい場合はそれなりのコストを伴う処理です)。

<input>のような単一DOM要素であれば、そもそも2の比較ステップ自体がないので問題ないですが、複合的なコンポーネントでは、潜在的にパフォーマンスの問題をはらんでいます。そこで、↑のコードでは、イベントハンドラをメソッドで定義しているのです。一般的に、コンポーネントに対してコールバックを定義するときには、匿名関数ではなくメソッドで定義しておいたほうが無難です。

実行結果

holding-state.png

Lessen 8: 条件に応じて表示する要素を変える

JSXの{}内には式しか記述できないため、ifのような制御構文は置けません。
そのため、propsやstateに応じて、出力する要素を変えたいときには、三項演算子(a ? b : c)を使います。

import React, { Component } from 'react'
import ReactDOM from 'react-dom'

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      checked: false,
    }
  }
  render() {
    return (
      <div>
        <label>
          Reactがわかってきた<input
            type="checkbox"
            onChange={event => this.setState({ checked: event.target.checked })}
          />
        </label>
        <br />
        {this.state.checked ? (
          <img src="https://media.giphy.com/media/11ISwbgCxEzMyY/giphy.gif" />
        ) : (
          <p>Reactやっていきましょう</p>
        )}
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('app'))

もちろん、JSXの外側でふつうにif文を使ってもかまいません。
条件が複雑になる場合は、こちらのほうがいいでしょう。

import React, { Component } from 'react'
import ReactDOM from 'react-dom'

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      checked: false,
    }
  }
  render() {
    let variableElement
    if (this.state.checked) {
      variableElement = (
        <img src="https://media.giphy.com/media/11ISwbgCxEzMyY/giphy.gif" />
      )
    } else {
      variableElement = <p>Reactやっていきましょう</p>
    }

    return (
      <div>
        <label>
          Reactがわかってきた<input
            type="checkbox"
            onChange={event => this.setState({ checked: event.target.checked })}
          />
        </label>
        <br />
        {variableElement}
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('app'))

conditional-rendering.png

☝️ワンポイント

<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>

このJSXは、

React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)

このようなJavaScriptコードに変換されます。
JSXと言えどもただのオブジェクトなので、ふつうに変数に入れられます。

Lesson 9: リストを表示する

配列に格納された値を元にコンポーネントのリストを表示するときには、mapを使います。
コンポーネントの配列をレンダリングするときには、各要素に値の被らないkeyプロパティーを指定する必要があります。

import React from 'react'
import ReactDOM from 'react-dom'

const cats = [
  { id: 1, name: 'mee' },
  { id: 2, name: 'gomoku' },
  { id: 3, name: 'tama' },
  { id: 4, name: 'mike' },
  { id: 5, name: 'mikael' },
]

function HelloMessage(props) {
  return <p>Hello, {props.target}</p>
}

function App(props) {
  return (
    <ul>
      {cats.map(cat => (
        <li key={cat.id}>
          <HelloMessage target={cat.name} />
        </li>
      ))}
    </ul>
  )
}

ReactDOM.render(<App />, document.getElementById('app'))

実行結果

list-rendering.png

☝️ワンポイント

keyプロパティーとして、配列のインデックスを指定するのは、非推奨です(eslintなどでは警告が表示されます)。
これをやると、リストの要素を並びかえたり、要素を挿入したりしたときに、パフォーマンスが下がったり、表示結果がおかしくなることがあるからです。そのため、配列要素には、その要素を特定できる何らかの値を格納しておく必要があります。

番外編: Reduxで状態を管理する

将来のことはわかりませんが、すくなくとも現時点において、Reactアプリ開発の現場では、Reduxという状態管理のためのライブラリが併用されることが多いと思います。

試しにLesson 7のコードをRedux化してみます。Reduxを使うためには追加パッケージが2つ必要です。

npm install --save redux react-redux

Reduxが入ると、概念的にもコード的にも一気に複雑になります。
これ自体が非常に奥深いテーマなので、ここでは詳細な説明はしません。

Redux導入のメリットとして感じていることを列挙すると:

  • アプリ状態変更がActionという概念でまとめられるので、見通しが良くなる
  • propsのバケツリレーが避けられる(Reduxがバケツリレーを避ける唯一の手段という意味ではないです)
  • PureComponentと似たパフォーマンス最適化が自動的に得られる
  • 強力なデバッグツールが付いてくる

といった感じです。
Reduxを導入すると、下図のように状態の置き場所と流れが変わります。

react-redux.png

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider, connect } from 'react-redux'

// アクションを定義する
const CHANGE_NAME = 'CHANGE_NAME'
const CHANGE_ADDRESS = 'CHANGE_ADDRESS'

// アクションはJavaScriptオブジェクトで表現される。
// アクションを生成するためのAction Creatorと呼ばれる関数を定義する。
function createChangeName(name) {
  return {
    type: CHANGE_NAME,
    payload: {
      name,
    },
  }
}

function createChangeAddress(address) {
  return {
    type: CHANGE_ADDRESS,
    payload: {
      address,
    },
  }
}

// アプリの初期状態
const initialState = {
  name: '',
  address: '',
}

// アプリの状態変更は、Reducerと呼ばれる「純粋関数」で定義される。
// 純粋関数なので引数を変更してはいけない。
function reducer(state = initialState, action) {
  switch (action.type) {
    case CHANGE_NAME:
      return {
        ...state,
        name: action.payload.name,
      }
    case CHANGE_ADDRESS:
      return {
        ...state,
        address: action.payload.address,
      }
    default:
      return state
  }
}

// PersonForm,PersonInfoコンポーネントはLesson 7で定義したものとまったく同じ。
function PersonForm({ onNameChange, onAddressChange }) {
  return (
    <form>
      <label>
        指名: <input type="text" onChange={onNameChange} />
      </label>
      <label>
        住所: <input type="text" onChange={onAddressChange} />
      </label>
    </form>
  )
}

function PersonInfo({ name, address }) {
  const tableStyle = {
    borderCollapse: 'collapse',
  }
  const cellStyle = {
    border: 'solid black 1px',
    height: '1rem',
    padding: '0.3rem',
  }
  return (
    <table style={tableStyle}>
      <thead>
        <tr>
          <th style={cellStyle}>氏名</th>
          <th style={cellStyle}>住所</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td style={cellStyle}>{name}</td>
          <td style={cellStyle}>{address}</td>
        </tr>
      </tbody>
    </table>
  )
}

// コンポーネントを、アプリのグローバルな状態が保持されるStoreと接続する。
// mapStateToPropsは、Storeから、コンポーネントに渡すpropsを抜き出す関数
function mapStateToProps(state) {
  return {
    name: state.name,
    address: state.address,
  }
}
const PersonInfoConnected = connect(mapStateToProps, null)(PersonInfo)

function mapDispatchToProps(dispatch) {
  return {
    onNameChange: event => dispatch(createChangeName(event.target.value)),
    onAddressChange: event => dispatch(createChangeAddress(event.target.value)),
  }
}
const PersonFormConnected = connect(null, mapDispatchToProps)(PersonForm)

// Appコンポーネントは、自身で`state`を持たなくなったのでスッキリした。
function App() {
  return (
    <div>
      <PersonFormConnected />
      <hr />
      <PersonInfoConnected />
    </div>
  )
}

// Storeインスタンスを生成して、Appコンポーネントの外側に配置する。
// これで、内側のツリーのどこからでも、Storeを参照できるようになる。
const store = createStore(reducer)

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('app')
)

実行結果

holding-state.png

参考


  1. ちなみに、設定不要でReactを動かせる環境は、create-react-app, parcel, codesandboxなど、poi以外にもいろいろなものがあります。 ↩