jeremygo

jeremygo

我是把下一颗珍珠串在绳子上的人

React 入門

本文はゼロから React の核心知識を紹介します。以下は参考アウトラインです〜

  • React
    • React とは
    • なぜ React を使用するのか
  • プロジェクトプレビュー
  • JSX
  • スタイル
  • コンポーネント
    • props
  • クラスコンポーネント
    • State
    • イベント処理
    • ライフサイクル
  • もっと
    • スキャフォールディング
    • 状態管理とルーティング

それでは、さっそく始めましょう〜

React#

React とは#

公式定義:ユーザーインターフェースを構築するための JavaScript ライブラリ

定義から私たちが認識すべきことは、React 自体が行うのはユーザーインターフェースの構築だけであり、大規模な React プロジェクトは一般的にそのエコシステム(ルーティング: React-Router、状態管理ライブラリ: Redux など)と密接に結びついて実現されることです。この文章は主に React の核心知識に焦点を当てています。

なぜ React を使用するのか#

  • 仮想 DOM: 私たちは、JS が頻繁に DOM を操作するコストが非常に高いことを知っていますが、React が初めて提唱した仮想 DOM は、JS レベルで DOM を操作することを実現し、アプリケーションの効率を大幅に向上させました。
  • 再利用可能なコンポーネント: React の普及はコンポーネント化の考え方を促進しました。コンポーネント化の核心は再利用性にあり、従来の Web 開発モデルに比べてメンテナンスが容易で、開発効率を大いに向上させます。
  • Facebook によるメンテナンス: React は Facebook という大きな後ろ盾があり、多くの優れた開発者がメンテナンスとイテレーションを行っています。また、コミュニティも非常に活発で、開発中に直面するほとんどの問題はすぐに解決できます。
  • 現実:最後のポイントは国内の現状で、大手企業の技術スタックは基本的に React に基づいているため、会社に合わせるなら React は必須のスキルです。

プロジェクトプレビュー#

最終的なプロジェクトの効果を見てみましょう。原生 JS を使ってどのように実現するか考えてみてください?

preview

初期の空のテンプレート:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  
</body>
</html>

JSX#

まず、React がどのようにインターフェースを構築し、要素をレンダリングするかを見てみましょう:

  • React の構文は JSX であり、JavaScript と HTML が混在した構文です。

ここでは、外部リンクを使用して React プロジェクトの基本的な 2 つのリンクを追加します:

	<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>   // reactのコア
	<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>  // react-domブラウザ(DOM)のレンダリング

同時に、JSX の構文を直接使用できるようにするために、babel の外部リンクを追加する必要があります:

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>

// スクリプトにtypeを指定
<script type="text/babel">
// コーディング....
</script>

そして、最初の React コードを書きます:

ReactDOM.render(<h1>Hello React!</h1>, document.getElementById('app'));

ブラウザをリフレッシュします:

Hello React

ReactDOMオブジェクトはreact-domから来ており、ここではそのrenderメソッドを呼び出しています。最初の引数はレンダリングする要素、2 番目は実際の DOM オブジェクトです。これにより、JSX 要素がページに正常にレンダリングされました。

JSX 構文は実際には HTML に非常に似ていることがわかりますが、CSS はどうでしょうか?

スタイル#

インラインスタイル:

ReactDOM.render(<h1 style={{backgroundColor: 'lightblue'}}>Hello React!</h1>, document.getElementById('app'))

外部スタイル:

const h1Style = {
    backgroundColor: 'lightblue'
}

ReactDOM.render(<h1 style={h1Style}>Hello React!</h1>, document.getElementById('app'))

PS: CSS クラスセレクタを使用する場合、JSX での書き方はclassNameです(ES6 のclassと区別するため)。

現在、これらの書き方を学ぶだけで十分です。

コンポーネント#

先ほどrenderメソッドで直接書いた JSX が、JSX 要素が複雑になると、別々にComponentを定義する必要があります。まずは無状態コンポーネントの書き方を見てみましょう:

function App () {
  return (
    <div>
      <h1>Hello React!</h1>
      <p>reactはとても素晴らしいです!</p>
    </div>
  )
}

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

ブラウザをリフレッシュします:

awesome

ここで注意が必要なのは、返されるコンポーネントは必ず 1 つの最大のタグで包含されなければならないということです。

次に、少し面白いことをやってみましょう:

function App () {
  const books = ['データベース', 'データ構造', 'コンピュータネットワーク']
  return (
    <div>
      <h3>私の本: </h3>
      <ul>
        {books.map(book => 
          <li>{book}</li>
        )}  
      </ul>  
    </div>
  )
}

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

books配列を定義し、関数コンポーネント内で ES6 のmapメソッドを使用して対応するbook要素をループレンダリングします。

ブラウザをリフレッシュします:

books

まず、画面に 3 つのliが表示されているのがわかりますが、さらに目立つのはコンソールに表示された目立つエラーメッセージです。このエラーメッセージは非常に重要で、ループで生成された各要素にはkeyが必要であることを意味します。これにより、React はリストの変更時にメンバーの追加、変更、削除の操作を識別でき、より良いパフォーマンスを発揮します。したがって、ここではmapの 2 番目の引数を使用して対応するkeyを追加します:

function App () {
  const books = ['データベース', 'データ構造', 'コンピュータネットワーク']
  return (
    <div>
      <h3>私の本: </h3>
      <ul>
        {books.map((book, i) => 
          <li key={i}>{book}</li>
        )}  
      </ul>  
    </div>
  )
}

コンソールにエラーが表示されなくなりました。

ここで、Appコンポーネント内のループリストは、より良い再利用性を実現するために、別のリストコンポーネントとして抽出するのが適切であることもわかります:

function BookList () {
  return (
    <ul>
      {books.map((book, i) => 
        <li key={i}>{book}</li>
      )}  
    </ul>  
  )
}

function App () {
  const books = ['データベース', 'データ構造', 'コンピュータネットワーク']
  return (
    <div>
      <h3>私の本: </h3>
      <BookList />
    </div>
  )
}

しかし、明らかに別の問題が発生しました。books配列はAppコンポーネント内で定義されているため、BookListコンポーネントはその値をどのように取得するのでしょうか?

props#

上記の問題は、親子コンポーネント間で値をどのように渡すかということです。非常に直接的な考え方として、親コンポーネント内で子コンポーネントを配置する際に、いくつかのカスタムパラメータを渡すことができます:

function App () {
  const books = ['データベース', 'データ構造', 'コンピュータネットワーク']
  return (
    <div>
      <h3>私の本: </h3>
      <BookList list={books} />
    </div>
  )
}

次に、BookList子コンポーネント内で渡されたパラメータをキャッチします:

function BookList (props) {
  console.log('props: ', props)
  const books = props.list
  return (
    <ul>
      {books.map((book, i) => 
        <li key={i}>{book}</li>
      )}  
    </ul>  
  )
}

ブラウザをリフレッシュします:

props

OK!これがpropsです。コンソールにこのオブジェクトを表示しましたので、異なるコンポーネント間でデータを渡す方法がわかります。

さて、新しい問題を考えてみましょう:現在のデータはただ静かに渡されているだけで、異なるコンポーネントは単にそれを表示するだけです。もし私たちがこれらのデータを追加または削除する必要がある場合、React はどのようにこれらのデータが変更されたことを知り、UI を即座に更新するのでしょうか?

クラスコンポーネント#

React が私たちに提供するもう一つのコンポーネント、すなわちクラスコンポーネントを知りましょう。

クラスコンポーネントは ES6 のclassに由来しています。ここでは、Appコンポーネントをクラスコンポーネントに書き換える方法を見てみましょう:

class App extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      books: ['データベース', 'データ構造', 'コンピュータネットワーク']
    }
  }
  render () {
    return (
      <div>
        <h3>私の本: </h3>
        <BookList list={this.state.books} />
      </div>
    )
  }
}

React.Componentは React が提供する一般的なクラスで、すべての React クラスに必要な実装の詳細をカプセル化しています。クラスコンポーネントはこれを継承して実現し、renderメソッドをオーバーライドして返すコンポーネント要素を定義します。

State#

元のbooks配列は、constructorコンストラクタ内に配置され、このクラスコンポーネントの内部状態として使用されます:

this.state = {
	books: ['データベース', 'データ構造', 'コンピュータネットワーク']
}

statethisを使用してクラスにバインドされており、コンポーネント全体でstateにアクセスできます。コンポーネントのstateを変更するたびに、コンポーネントのrenderメソッドが再度実行され、コンポーネントが再レンダリングされます。では、stateを直接変更することはできるのでしょうか?

React には 2 つの重要な原則があります:一つは単方向データフロー、もう一つは明確な状態の変更です。私たちが **stateを変更する唯一の方法はsetState()を使用することです **。

setState

コンポーネントはrender内で最新のstateの情報を取得してレンダリングし、View層でsetStateを呼び出してstateを更新し、その後コンポーネントは再度renderメソッドを実行し、画面を更新します。

試してみましょう:

class App extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      books: ['データベース', 'データ構造', 'コンピュータネットワーク']
    }
  }
  render () {
    return (
      <div>
        <h3>私の本: </h3>
        <BookList list={this.state.books} />
        <button onClick={() => this.setState({ books: ['コンパイラ原理', 'オペレーティングシステム'] })}>変更</button>
      </div>
    )
  }
}

change

イベント処理#

setStateに関連するロジックが複雑になると、異なるコンポーネント間でsetStateを呼び出す必要がある場合、再利用性とメンテナンス性の観点から、イベント処理をカスタム関数として抽出して呼び出す必要があります。React では、イベント処理関数のプレフィックスはすべてhandleであり、リスナー関数のプレフィックスはonです:

class App extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      books: ['データベース', 'データ構造', 'コンピュータネットワーク'],
      input: ''
    }

    this.handleAddBook = this.handleAddBook.bind(this)
    this.handleRemoveBook = this.handleRemoveBook.bind(this)
    this.updateInput = this.updateInput.bind(this)
  }

  handleAddBook () {
    this.setState(currentState => {
      return {
        books: currentState.books.concat([this.state.input])
      }
    })
  }

  handleRemoveBook (name) {
    this.setState(currentState => {
      return {
        books: currentState.books.filter(book => book !== name)
      }
    })
  }

  updateInput (e) {
    this.setState({
      input: e.target.value
    })
  }

  render () {
    return (
      <div>
        <h3>私の本: </h3>
        <input 
          type="text"
          placeholder="新しい本"
          value={this.state.input}
          onChange={this.updateInput}
        />
        <button onClick={this.handleAddBook}>追加</button>
        <BookList 
          list={this.state.books}
          onRemoveBook={this.handleRemoveBook}
        />
      </div>
    )
  }
}

handleAddBookhandleRemoveBookは追加と変更の操作です。ここで特に強調すべきは、コンストラクタ内のこの 3 行のコードです:

this.handleAddBook = this.handleAddBook.bind(this)
this.handleRemoveBook = this.handleRemoveBook.bind(this)
this.updateInput = this.updateInput.bind(this)

カスタムクラスメソッド内でthis.setStateを呼び出したい場合、ここでのthisundefinedです。したがって、クラスコンポーネントのthisがクラスメソッド内でアクセスできるようにするために、thisをクラスメソッドにバインドする必要があります。コンストラクタ内に配置すれば、バインドはコンポーネントがインスタンス化されるときに 1 回だけ実行され、パフォーマンスの消耗が少なくなります。

OK!実際にはここまでで、プロジェクトプレビューで表示される内容を基本的に完成させることができました。今、あなたはさらなる改善を試みて、以下の効果を達成してください:

preview

もしあなたが完成したなら、以下のコードを参考にしてください:

function ActiveBooks (props) {
  return (
    <div>
      <h2>読書中の本</h2>
      <ul>
        {props.list.map((book, i) => (
          <li key={i}>
            <span>{book.name}</span>
            <button onClick={() => props.onRemoveBook(book.name)}>削除</button>
            <button onClick={() => props.onDeactive(book.name)}>読了</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

function InactiveBooks (props) {
  return (
    <div>
      <h2>読了した本</h2>
      <ul>
        {props.list.map((book, i) => (
          <li key={i}>
            <span>{book.name}</span>
            <button onClick={() => props.onActive(book.name)}>読書中</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

class App extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      books: [
        {
          name: 'データベース',
          active: true
        }, 
        {
          name: 'データ構造',
          active: true
        }, 
        {
          name: 'コンピュータネットワーク',
          active: true
        }],
      input: ''
    }

    this.handleAddBook = this.handleAddBook.bind(this)
    this.handleRemoveBook = this.handleRemoveBook.bind(this)
    this.handleToggleBook = this.handleToggleBook.bind(this)
    this.updateInput = this.updateInput.bind(this)
  }

  handleAddBook () {
    this.setState(currentState => {
      return {
        books: currentState.books.concat([{
          name: this.state.input,
          active: true
        }]),
        input: ''
      }
    })
  }

  handleRemoveBook (name) {
    this.setState(currentState => {
      return {
        books: currentState.books.filter(book => book.name !== name)
      }
    })
  }

  handleToggleBook (name) {
    this.setState(currentState => {
      const book = currentState.books.find(book => book.name === name)

      return {
        books: currentState.books.filter(book => book.name !== name)
        .concat([{
          name,
          active: !book.active
        }])
      }
    })
  }

  updateInput (e) {
    this.setState({
      input: e.target.value
    })
  }

  render () {
    return (
      <div>
        <h3>私の本: </h3>
        <input 
          type="text"
          placeholder="新しい本"
          value={this.state.input}
          onChange={this.updateInput}
        />
        <button onClick={this.handleAddBook}>追加</button>
        <button onClick={() => this.setState({ books: [] })}> すべてクリア </button>
        <ActiveBooks 
          list={this.state.books.filter(book => book.active)}
          onRemoveBook={this.handleRemoveBook}
          onDeactive={this.handleToggleBook}
        />
        <InactiveBooks 
          list={this.state.books.filter(book => !book.active)}
          onActive={this.handleToggleBook}
        />
      </div>
    )
  }
}

ここまで来ると、あなたは自分でいくつかの小さなデモを書いて慣れることができるでしょう。それでは、最後の質問を考えてみましょう:

  • プロジェクトでは、多くの場合データがバックエンドとやり取りされます。つまり、非同期操作が行われ、データがまだリクエストされていないときにローディングスタイルを表示し、リクエストが完了した後に UI を更新したいと考えています。このようなロジックはどこに置くべきでしょうか?

ライフサイクル#

上記の問題に対して、私たちが実際に望むのは、コンポーネントが DOM にマウントされた後に画面をレンダリングし、同時に多くのコンポーネントを持つアプリケーションでは、コンポーネントが破棄されるときにその占有していたリソースを解放する必要があるということです。これが React のライフサイクルにおいて非常に重要な 2 つの関数、componentDidMountcomponentWillUnmountです。

ライフサイクル関数の実行プロセスを全体的に感じてみましょう:

class App extends React.Component {
  constructor (props) {
    ......

    console.log('--constructor--')
  }

  componentDidMount () {
    console.log('--componentDidMount--')
  }

  componentDidUpdate () {
    console.log('--componentDidUpdate--')
  }

  componentWillUnmount () {
    console.log('--componentWillUnmount--')
  }

 ......

  render () {
    console.log('--render--')

    return (
      ......
    )
  }
}

life-cycle

コンポーネントの全体的なライフサイクルは、constructor --> render --> componentDidMount、その後コンポーネントが更新されると再度render --> componentDidUpdate、コンポーネントが破棄される前にcomponentWillUnmountが呼び出されることがわかります。

次に、これらの関数を深く使用していきます。

まず、API を手動でシミュレートしてみましょう:

  window.API = {
    fetchBooks () {
      return new Promise((res, rej) => {
        const books =  [
          {
            name: 'データベース',
            active: true
          }, 
          {
            name: 'データ構造',
            active: true
          }, 
          {
            name: 'コンピュータネットワーク',
            active: false
          }
        ]
        setTimeout(() => res(books), 2000)
      })
    }
  }

次に、componentDidMount関数内でそれを呼び出します:

componentDidMount () {
  console.log('--componentDidMount--')
  
  API.fetchBooks()
    .then(books => {
      this.setState({
        books
      })
    })
}

API

componentDidMountの後にデータをリクエストし、その後renderが再度実行され、componentDidUpdateが実行されるのがわかります。

ユーザー体験を向上させるために、ローディングのロジックを追加してみましょう:

class App extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      books: [],
      loading: true,
      input: ''
    }

    ......

    console.log('--constructor--')
  }

  componentDidMount () {
    console.log('--componentDidMount--')
    
    API.fetchBooks()
      .then(books => {
        this.setState({
          books,
          loading: false
        })
      })
  }

  componentDidUpdate () {
    console.log('--componentDidUpdate--')
  }

  componentWillUnmount () {
    console.log('--componentWillUnmount--')
  }
  
  ......

  render () {
    console.log('--render--')

    if (this.state.loading === true) {
      return <h2>ローディング中...</h2>
    }

    return (
      ......
    )
  }
}

Loading

OK!これで React 入門の全過程が終了しました。もちろん、プレビューの効果を完全に実現したわけではありませんが、Loadingコンポーネントを独自に封装することをお勧めします。最後に、さらなる開発操作について簡単にお話ししましょう。

もっと#

スキャフォールディング#

私たちの入門チュートリアルは、React を使用するための従来の外部リンク引き込み方式を使用しています。また、JSX を使用するために babel を引き込む必要があります。現代の Web 開発プロセスは、Webpackのモジュール化構築とデプロイプロセスに基づいています。実際のプロジェクトでは、公式のスキャフォールディングcreate-react-appを使用して一歩で構築し、依存関係のインストールと環境のデプロイプロセスを簡素化し、コードロジックの作成により多くの焦点を当てることが一般的に推奨されます。

状態管理とルーティング#

React の定義を覚えていますか?それはユーザーインターフェースの構築にのみ焦点を当てています。クラスコンポーネントを使用して一定の内部状態を管理できますが、プロジェクトがある程度複雑になると、外部の状態管理ライブラリを導入せざるを得ません。ここでは、React の理念に合ったReduxの使用をお勧めします。また、現在のシングルページアプリケーションではルーティング管理が必要ですので、React-Routerの使用をお勧めします。

最後に言いたいのは、フロントエンド技術は表面的には非常に速く進化していますが、内部の原理は基本的に変わらないということです。React は新しい革新的な開発方法をもたらしました。この出発点を基に、React の設計理念を結びつけて、さらに多くの特性を深く理解していくことを願っています。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。