Reactの高度な概念と実践ガイド

React開発の基礎知識

Reactとは

Reactはユーザーインターフェースを構築するためのJavaScriptライブラリです。2013年にFacebookによってオープンソースとして公開され、現在では多くの大手企業で採用されています。

Reactの主な特徴:

  • 宣言的プログラミング:UI = f(state) の考え方
  • コンポーネントベース開発:複雑なUIを小さなコンポーネントに分割
  • クロスプラットフォーム対応:Web、React Native(モバイル)、React VR(VR)など

Hello Reactの基本例

従来のJavaScript実装


<h2 id="text"></h2>
<button id="btn">テキスト変更</button>
<script>
  // 命令型プログラミングの例
  let message = 'こんにちは世界'
  let textElement = document.getElementById("text")
  let btnElement = document.getElementById("btn")
  
  textElement.innerHTML = message
  
  btnElement.onclick = function () {
    message = 'こんにちはReact'
    textElement.innerHTML = message
  }
</script>

Reactを使用した実装


<div id="app"></div>
<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">
  let greeting = "こんにちは世界"

  function changeText() {
    greeting = 'こんにちはReact'
    renderApp()
  }

  function renderApp() {
    ReactDOM.render(
      <div>
        <h2>{greeting}</h2>
        <button onClick={changeText}>テキスト変更</button>
      </div>, 
      document.getElementById('app')
    )
  }

  renderApp()
</script>

コンポーネントを使用した実装


<div id="app"></div>
<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">
  class WelcomeApp extends React.Component {
    constructor() {
      super();
      this.state = {
        message: "こんにちは世界"
      }
    }

    render() {
      return (
        <div>
          <h2>{this.state.message}</h2>
          <button onClick={this.updateMessage.bind(this)}>テキスト変更</button>
        </div>
      )
    }

    updateMessage() {
      this.setState({
        message: 'こんにちはReact'
      })
    }
  }

  ReactDOM.render(<WelcomeApp/>, document.getElementById("app"))
</script>

JavaScriptコア構文

ES6のクラス


class User {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  introduce() {
    console.log(`名前:${this.name};年齢:${this.age}`)
  }
}

const user = new User('田中太郎', 25);
user.introduce()

クラスの継承


class Person {
  constructor(name) {
    this.name = name
  }

  walk() {
    console.log('親クラスのメソッド')
  }
}

class Teacher extends Person {
  constructor(name, subject) {
    super(name);
    this.subject = subject
  }

  teach() {
    console.log(`${this.name}先生は${this.subject}を教えています`)
  }
}

const teacher = new Teacher('佐藤花子', '数学');
teacher.walk()
teacher.teach()

JSXコア構文

映画リストの例


<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
class MovieList extends React.Component {
  constructor() {
    super();
    this.state = {
      title: "人気映画",
      movies: ['千と千尋の神隠し', 'もののけ姫', '風の谷のナウシカ']
    }
  }

  render() {
    const movieItems = this.state.movies.reduce((list, film) => {
      list.push(<li>{film}</li>)
      return list
    }, [])
    
    return (
      <div>
        <div>
          <h2>{this.state.title}リスト1</h2>
          <ul>{movieItems}</ul>
        </div>
        <div>
          <h2>{this.state.title}リスト2</h2>
          <ul>{
            this.state.movies.map(film => <li>{film}</li>)
          }</ul>
        </div>
      </div>
    )
  }
}

ReactDOM.render(<MovieList/>, document.getElementById("app"));
</script>

カウンターアプリの例


<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
class CounterApp extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 0
    }
  }

  render() {
    return (
      <div>
        <div>カウント: {this.state.count}</div>
        <button onClick={this.increment.bind(this)}>増加</button>
        <button onClick={this.decrement.bind(this)}>減少</button>
      </div>
    )
  }

  increment() {
    this.setState({
      count: this.state.count + 1
    })
  }

  decrement() {
    this.setState({
      count: this.state.count - 1
    })
  }
}

ReactDOM.render(<CounterApp/>, document.getElementById("app"));
</script>

JSXの基本

JSXはJavaScriptの構文拡張であり、React.createElement()関数の糖衣構文です。JSXを使用すると、JavaScriptコード内にHTMLのような構造を記述できます。


<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
const element = <div>こんにちは世界</div>
ReactDOM.render(element, document.getElementById("app"));
</script>

ReactがJSXを選択した理由

Reactは、レンダリングロジックが本質的に他のUIロジックと密接に結びついていると考えています(All in JS)。これらは不可分であるため、Reactはマークアップを別のファイルに分離するのではなく、コンポーネントとしてまとめています。

JSXの記述規則:

  • 最上位にはルートタグが1つだけ必要(通常はdivまたはFragment)
  • JSXを括弧()で囲むと可読性が向上し、改行も可能
  • 自己終了タグは必ず/>で終わる

JSXでのコメント


<script type="text/babel">
/**
 * ドキュメントコメント
 */
class CommentExample extends React.Component {
  constructor() {
    super();
    /*
    // 複数行コメント
    this.state = {
    }*/
  }

  // 単一行コメント
  render() {
    return (
      <div>
        {/* JSXのコメント */}
      </div>
    )
  }
}

ReactDOM.render(<CommentExample/>, document.getElementById("app"));
</script>

JSXへのデータ埋め込み


<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
class DataDisplay extends React.Component {
  constructor() {
    super();
    this.state = {
      // 1、{}内で正常に表示できる内容
      name: '山田太郎',// 文字列
      age: 30,// 数値
      hobbies: ['読書', '旅行', '料理'],// 配列
      // 2、{}内で表示できない(無視される)内容
      test1: null,// null
      test2: undefined,// undefined
      test3: true,// 真偽値
      flag: true,
      // 3、オブジェクトはJSXの子要素として使用できない
      friend: {
        name: '鈴木一郎',
        age: 28
      }
    }
  }

  render() {
    return (
      <div>
        <div>{this.state.name}</div>
        <div>{this.state.age}</div>
        <div>{this.state.hobbies}</div>
        <div>{this.state.test1}</div>
        <div>{String(this.state.test1)}</div>
        <div>{this.state.test2}</div>
        <div>{this.state.test2 + ''}</div>
        <div>{this.state.test3}</div>
        <div>{this.state.test3.toString()}</div>
        <div>{this.state.flag ? 'ログイン中' : null}</div>
        <div>{this.state.flag && 'ようこそ'}</div>
        {/* <div>{this.state.friend}</div> */}
      </div>
    )
  }
}

ReactDOM.render(<DataDisplay/>, document.getElementById("app"));
</script>

JSXへの式の埋め込み


<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
class ExpressionExample extends React.Component {
  constructor() {
    super();
    this.state = {
      firstName: '田中',
      lastName: '太郎',
      isLoggedIn: true
    }
  }

  render() {
    const {firstName, lastName, isLoggedIn} = this.state
    return (
      <div>
        {/* 1、演算子式 */}
        <div>{firstName + ' ' + lastName}</div>
        <div>{20 * 50}</div>
        {/* 2、三項演算子 */}
        <div>{isLoggedIn ? 'お帰りなさい~' : 'ログインしてください~'}</div>
        {/* 3、関数呼び出し */}
        <div>{this.getFullName()}</div>
      </div>
    )
  }

  getFullName() {
    return this.state.firstName + ' ' + this.state.lastName
  }
}

ReactDOM.render(<ExpressionExample/>, document.getElementById("app"));
</script>

JSXでの属性バインディング


<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
function getImageUrl(url, size) {
  return url + `?param=${size}x${size}`
}

class AttributeBinding extends React.Component {
  constructor() {
    super();
    this.state = {
      title: "タイトル",
      imageUrl: "https://example.com/image.jpg",
      link: "https://www.google.com",
      isActive: true
    }
  }

  render() {
    const {title, imageUrl, link, isActive} = this.state
    return (
      <div>
        {/* 1、通常の属性バインディング */}
        <h2 title={title}>見出し</h2>
        <img src={getImageUrl(imageUrl, 140)} alt="画像"/>
        <a href={link} target="_blank">Google検索</a>
        {/* 2、クラス名のバインディング */}
        <div className="box title">div要素</div>
        <div className={"box title " + (isActive ? 'active' : "")}>div要素2</div>
        <label htmlFor="inputId"></label>
        {/* 3、スタイルのバインディング */}
        <div style={{color: 'red', fontSize: '50px'}}>スタイル属性をバインドしたdiv</div>
      </div>
    )
  }
}

ReactDOM.render(<AttributeBinding/>, document.getElementById("app"));
</script>

JSXでのイベントバインディングとthisの処理


<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
class EventHandling extends React.Component {
  constructor() {
    super();
    this.state = {
      message: "こんにちは",
      counter: 100
    }
    this.handleClick = this.handleClick.bind(this)
  }

  render() {
    return (
      <div>
        {/* 1、bindでthisをバインド(明示的バインディング) */}
        <button onClick={this.handleClick}>ボタン1</button>
        {/* 2、アロー関数を使用して関数を定義 */}
        <button onClick={this.increment}>増加</button>
        {/* 3、アロー関数を直接渡す(推奨) */}
        <button onClick={() => {
          this.decrement('田中')
        }}>減少
        </button>
      </div>
    )
  }

  handleClick() {
    console.log(this.state.message)
  }

  // アロー関数はthisをバインドしない
  // ES6のクラスフィールド構文
  increment = () => {
    console.log(this.state.counter)
  }

  decrement(name) {
    console.log(this.state.counter, name)
  }
}

ReactDOM.render(<EventHandling/>, document.getElementById("app"));
</script>

JSXでのイベントバインディング - パラメータ渡し


<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
class ParameterPassing extends React.Component {
  constructor() {
    super();
    this.state = {
      users: ['田中太郎', '佐藤花子', '山田次郎']
    }
    this.handleButtonClick = this.handleButtonClick.bind(this)
  }

  render() {
    return (
      <div>
        <button onClick={this.handleButtonClick}>ボタン1</button>
        <ul>{
          this.state.users.map((user, index) => {
            return (
              <li
                className="item"
                title="リスト項目"
                onClick={e => {
                  this.handleItemClick(user, index, e)
                }}>
                {user}
              </li>
            )
          })
        }</ul>
      </div>
    )
  }

  handleButtonClick(event) {
    console.log('ボタンがクリックされました', event)
  }

  handleItemClick(user, index, event) {
    console.log('リスト項目がクリックされました', user, index, event)
  }
}

ReactDOM.render(<ParameterPassing/>, document.getElementById("app"));
</script>

条件付きレンダリング


<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
class ConditionalRendering extends React.Component {
  constructor() {
    super();
    this.state = {
      isLoggedIn: true
    }
  }

  render() {
    const {isLoggedIn} = this.state
    // 1、if文による条件分岐:ロジックが複雑な場合
    let welcomeMessage = null
    if (isLoggedIn) {
      welcomeMessage = <h2>お帰りなさい~</h2>
    } else {
      welcomeMessage = <h2>ログインしてください~</h2>
    }

    return (
      <div>
        {welcomeMessage}
        {/* 2、三項演算子 */}
        <button onClick={e => this.toggleLogin()}>{isLoggedIn ? 'ログアウト' : 'ログイン'}</button>
        <hr/>
        <h2>{isLoggedIn ? 'こんにちは、田中さん' : null}</h2>
        {/* 3、論理積(&&)演算子 */}
        <h2>{isLoggedIn && 'こんにちは、佐藤さん'}</h2>
        {isLoggedIn && <h2>こんにちは、山田さん</h2>}
      </div>
    )
  }

  toggleLogin() {
    this.setState({
      isLoggedIn: !this.state.isLoggedIn
    })
  }
}

ReactDOM.render(<ConditionalRendering/>, document.getElementById("app"));
</script>

条件付きレンダリング - v-show効果


<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
class VisibilityToggle extends React.Component {
  constructor() {
    super();
    this.state = {
      isVisible: true
    }
  }

  render() {
    const {isVisible} = this.state
    const displayStyle = isVisible ? 'block' : 'none'
    return (
      <div>
        <button onClick={e => this.toggleVisibility()}>
          {isVisible ? '非表示' : '表示'}
        </button>
        <h2 style={{display: displayStyle}}>こんにちは、田中さん</h2>
      </div>
    )
  }

  toggleVisibility() {
    this.setState({
      isVisible: !this.state.isVisible
    })
  }
}

ReactDOM.render(<VisibilityToggle/>, document.getElementById("app"));
</script>

リストレンダリング


<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
class ListRendering extends React.Component {
  constructor() {
    super();
    this.state = {
      names: ['田中太郎', '佐藤花子', '山田次郎'],
      numbers: [5, 8, 3, 4, 9, 2, 7, 6, 1]
    }
  }

  render() {
    return (
      <div>
        <h2>名前リスト(マッピング)</h2>
        <ul>{this.state.names.map(name => <li key={name}>{name}</li>)}</ul>
        <h2>数字リスト(フィルタリング)</h2>
        <ul>{this.state.numbers.filter(num => num >= 5).map(num => <li key={num}>{num}</li>)}</ul>
        <h2>数字リスト(スライス)</h2>
        <ul>{this.state.numbers.slice(0, 5).map(num => <li key={num}>{num}</li>)}</ul>
      </div>
    )
  }
}

ReactDOM.render(<ListRendering/>, document.getElementById("app"));
</script>

JSXの本質

JSXの本質


<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
/**
 * 1、JSXはReact.createElement(component, props, ...children)関数の糖衣構文です
 *     - JSX -> babel -> React.createElement()
 * 2、Babel公式サイトで変換プロセスを確認できます
 *     - https://babeljs.io/repl/#?presets=react
 */
const message1 = <h2>こんにちは世界</h2>
const message2 = React.createElement("h2", null, "こんにちは世界")
ReactDOM.render(message2, document.getElementById("app"));
</script>

仮想DOMの作成プロセス


<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
class VirtualDOMDemo extends React.Component {
  constructor() {
    super();
  }

  /**
   * 1、React.createElementを通じて最終的にReactElementオブジェクトが作成されます
   * 2、このReactElementオブジェクトの役割は何でしょうか?Reactがなぜこれを作成するのでしょうか?
   *     - ReactはReactElementオブジェクトを使ってJavaScriptのオブジェクトツリーを構成するため
   *     - このJavaScriptのオブジェクトツリーが仮想DOM(virtual dom)です
   * 3、ReactElementのツリー構造をどのように確認するのでしょうか?
   *     - JSXの戻り値をコンソールに出力して確認できます
   * 4、ReactElementが最終的に形成するツリー構造が仮想DOMです
   * 5、jsx-仮想dom-実際のdom
   *     - jsxコード-ReactElementオブジェクト-実際のdom
   * 6、なぜ仮想DOMを使用し、直接実際のDOMを操作しないのでしょうか?
   *     - 状態の変化を追跡するのが困難:従来の開発モデルでは、状態の変化を追跡するのが難しく、アプリケーションのデバッグが不便
   *     - 実際のDOM操作のパフォーマンスが低い:従来の開発モデルでは頻繁なDOM操作が行われ、これは非常にパフォーマンスが低い
   *         ~ document.createElement自体が非常に複雑なオブジェクトを作成する
   *         ~ DOM操作はブラウザのリフローと再描画を引き起こすため、開発では頻繁なDOM操作を避けるべき
   * 7、宣言的プログラミング
   *     - 仮想DOMはプログラミング理念です
   *         ~ この理念では、UIは理想的または仮想化された方法でメモリに保持され、比較的単純なJavaScriptオブジェクトです
   *         ~ ReactDOM.renderを通じて仮想DOMと実際のDOMを同期させることができます。このプロセスを調整(Reconciliation)と呼びます
   *     - このプログラミング方法はReactに宣言的なAPIを提供します
   *         ~ UIをどのような状態にしたいかだけをReactに伝えればよい
   *         ~ ReactがDOMとこれらの状態が一致することを保証します
   *         ~ 手動でのDOM操作、属性操作、イベント処理から解放されます
   */
  render() {
    const virtualTree = (
      <div>
        <h2>東京</h2>
        <h2>大阪</h2>
        <h2>京都</h2>
      </div>
    )
    console.log(virtualTree)
    return virtualTree
  }
}

ReactDOM.render(<VirtualDOMDemo/>, document.getElementById("app"));
</script>

実践例:ショッピングカート


<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Reactショッピングカート</title>
  <style>
  table {
    border-collapse: collapse;
    width: 100%;
  }

  td, th {
    border: 1px solid #cccccc;
    padding: 10px 16px;
    text-align: left;
  }

  th {
    background: #f2f2f2;
  }

  .quantity {
    margin: 0 6px;
  }
  
  button {
    padding: 5px 10px;
    margin: 0 5px;
  }
  </style>
</head>
<body>
<div id="app"></div>
<script src="./js/react.development.js" crossorigin></script>
<script src="./js/react-dom.development.js" crossorigin></script>
<script src="./js/babel.min.js"></script>
<script type="text/babel">
class ShoppingCart extends React.Component {
  constructor() {
    super();
    this.state = {
      products: [
        {
          id: 1,
          name: 'JavaScript入門',
          date: '2022-01',
          price: 2980,
          quantity: 1
        }, 
        {
          id: 2,
          name: 'React実践ガイド',
          date: '2022-02',
          price: 3580,
          quantity: 1
        }, 
        {
          id: 3,
          name: 'CSSデザインブック',
          date: '2022-03',
          price: 2580,
          quantity: 1
        }, 
        {
          id: 4,
          name: 'Node.js徹底解説',
          date: '2022-04',
          price: 4280,
          quantity: 1
        }
      ]
    }
  }

  render() {
    return this.state.products.length ? (
      <div>
        <table>
          <thead>
            <tr>
              <th>ID</th>
              <th>商品名</th>
              <th>出版日</th>
              <th>価格</th>
              <th>数量</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            {this.state.products.map((item, index) => {
              return (
                <tr key={item.id}>
                  <td>{item.id}</td>
                  <td>{item.name}</td>
                  <td>{item.date}</td>
                  <td>¥{item.price.toLocaleString()}</td>
                  <td>
                    <button 
                      disabled={item.quantity <= 1} 
                      onClick={() => this.updateQuantity(index, -1)}
                    >-</button>
                    <span className="quantity">{item.quantity}</span>
                    <button onClick={() => this.updateQuantity(index, 1)}>+</button>
                  </td>
                  <td>
                    <button onClick={e => this.removeProduct(e, index)}>削除</button>
                  </td>
                </tr>
              )
            })}
          </tbody>
        </table>
        <h3>合計金額:¥{this.calculateTotal().toLocaleString()}</h3>
      </div>
    ) : (
      <h2>ショッピングカートは空です~</h2>
    )
  }

  updateQuantity(index, change) {
    const updatedProducts = [...this.state.products]
    updatedProducts[index].quantity += change
    this.setState({
      products: updatedProducts
    })
  }

  removeProduct(e, index) {
    e.persist()
    // Reactの設計原則:stateのデータは不変
    this.setState({
      products: this.state.products.filter((item, idx) => idx !== index)
    })
  }

  calculateTotal() {
    const totalPrice = this.state.products.reduce((total, item) => {
      return total += item.price * item.quantity
    }, 0)
    return totalPrice
  }
}

ReactDOM.render(<ShoppingCart/>, document.getElementById("app"));
</script>
</body>
</html>

タグ: React JSX ES6 javascript 仮想DOM

7月4日 16:30 投稿