ヒキダスブログ

テック系や最近見たもの感じたことを書いて残す引き出しスペースです

React x TypeScript 学習 (4) - Reactの基本構文

概要

久しぶりにReactを触った備忘録として、Reactの基本構文についておさらいしてみようと思います。
今回触れる内容としてはこちらになります。

  1. エントリーポイントの記述
  2. コンポーネントの基本構文 (Class ComponentとFunctional Component)
  3. コンポーネントの状態管理 (State)
  4. コンポーネントより受け取るプロパティ (Props)
  5. エレメント取得 (Refs)

基本構文

1. エントリーポイントの記述

Reactで作ったコンポーネントを描画するにあたり、まずは「どのDOM要素に描画するか」をエントリーポイントで指定する必要があります。第3回までのソースコードでいうと、/src/index.tsxがエントリーポイントに相当します。

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

import App from './App'

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

コンポーネント作成なら react パッケージで良いのですが、DOM要素への描画にあたり react-dom も必要となります。上述ですと、「id="app"のDOM要素の中に Appコンポーネントを出力する」処理となります。

2. コンポーネントの基本構文

ReactにしてもVueにしても、コンポーネントについて個人的ざっくりイメージで、ページからUIパーツまで流用しやすい粒度でビュー・スタイル・ロジックをセットにしたもの と定義しています。

これまでのフロントエンド開発を振り返ると、html, css, jsを「関心の分離(Separation of concerns)」に基づいて管理していました。それより前だとhtmlのタグに対してonclickでJSのロジックを埋め込んだりインラインスタイルで書くこともありましたが、その場合htmlとcss, jsが密結合になり、コードの可読性や問題があった時の切り分けがしづらくなることから、html, css, jsをそれぞれ別ファイルで管理しようという流れになっていったようです。
一方で、UI開発において類似したパーツを流用しようとなると、そうして別ファイルとなっているビュー・スタイル・ロジックをそれぞれコピペする等になりコードも重複になり煩雑になってしまいます。
ReactやVueではそうした「関心の分離」から「技術の分離(Separation of technologies)」として、ビューとロジックをセットにして必要な時に読み込む書き方でコンポーネント開発を推し進めました。

話がそれてしまいましたが、Reactではコンポーネント開発にあたり「単一ファイル内にビューとロジックをセットに書く」ということを念頭においてもらうと良いでしょう。そして、そのReactコンポーネントの定義方法には2種類あります。
先ほどのエントリーポイントで読み込んだAppコンポーネント(/src/App.tsx)で書いてみようと思います。

Functional Component

import React from 'react'

const App = () => {
  return (
    <p>This is App component.</p>
  )
}

export default App

Functionalというように関数形式で書かれたコンポーネントの定義方法になります。reactパッケージを読み込んでからApp関数で返す要素を記述しそれをエクスポートする形になります。
関数には上述のような変数に代入するタイプもありますし、以下のようにfunctionを使った定義方法でもOKです。

import React from 'react'

function App() {
  return (
    <p>This is App component.</p>
  )
}

export default App

以下のケースでこの書き方が有用になります。

これまでこの書き方のネックなところは状態(State)を持つことができず、その場合はClass Componentに置き換えるなりして対応していたのが、React 16.8.0〜の機能である React Hooks でそれも解消されるようになりました。React Hooksは調べ次第後日アップしようと思います。

Class Component

import React from 'react'
  
export default class App extends React.Component {
  render() {
    return(
      <p>This is App component.</p>
    )
  }
}

こちらはclass構文を使ったコンポーネントの定義方法になります。Functional Componentの違いとしては以下が挙げられるでしょう。

  • ReactのComponentを継承する(他にもPureComponentを継承するケースもある)
  • renderメソッド内で返却する要素を記述する
  • Reactのライフサイクル(マウントされた直後に走るcomponentDidMount他)にフックして処理を書き足せる

class構文であることやライフサイクルフックもあり、コンポーネントの機能としてはもりもりでその分、機能によっては記述が肥大化しやすい面が見受けられます。その場合は、その中からコンポーネントとしてさらに切り出せないか検討する必要がありそうです。私もこの書き方で長尺なものを見るとウッとなってしまうのですが、「結論(描画結果)はrender見ればいいよね」と割り切るようにしました。この体験がReactから敬遠していた理由の一つですね。
ライフサイクルフックも時間があれば、別の回でまとめようと思います。

3. コンポーネントの状態管理 (State)

例えば、トグルボタンのコンポーネントだと「トグルがONになっているかどうか」、インプットフォームなら「現在のインプット情報」などなど、コンポーネントによって内部に閉じた状態を管理する必要が出てきます。
ここではトグルボタンを例にして、状態であるStateの扱いを書いてみます。

まずは、先ほど用意したAppコンポーネントを以下のように別ファイルのToggleコンポーネントを表示するよう書き換えます。

import React from 'react'

import Toggle from './components/Toggle'

const App = () => {
  return (
    <Toggle />
  )
}

export default App

そして、componentsディレクトリ配下にToggle.tsxファイルを作成し、以下のように記述します。

import React from 'react'

interface State {
  isToggleOn: boolean
}

export default class Toggle extends React.Component<{}, State> {
  state: State = {
    isToggleOn: false
  }

  render() {
    const currentState: string = this.state.isToggleOn ? 'ON': 'OFF'
    return (
      <div>
        <p>{currentState}</p>
        <button>Toggle</button>
      </div>
    )
  }
}

状態を持たせる点よりClass Componentで定義しました。interfaceというのはTypeScriptで使用できる型定義方法の一つになります。

その状態ですが、stateというプロパティでReactでは管理しています。class構文直下に書かれているstateプロパティがToggleコンポーネントの「状態」にあたり、isToggleOnの値を保持しています。これは「ToggleがONになっているか」を意味していて、作るコンポーネントによって任意に保持するキー・値を決めていきます。この状態はthis.state.[state内のキー]で値を参照することができます。

state: State = {
  isToggleOn: false
}  

そして、renderメソッドではreturnの前にcurrentState変数を定義し、先のthis.state.[state内のキー]を使ってトグルがtrueなら"ON"、falseなら"OFF"を代入するようにしています。
return内で出力する要素内でJSの記述を使うには中波括弧({})で囲ってやる必要があり、currentState変数の値を出力しています。

render() {
  const currentState: string = this.state.isToggleOn ? 'ON': 'OFF'
  return (
    <div>
      <p>{currentState}</p>
      <button>Toggle</button>
    </div>
  )
}

出力結果は、Toggleという名前のボタンの上に「OFF」という文字列が表示されて見えると思います。

その次にstateの値を更新するにはどうするか。ここでは、Toggleボタンを押すたびに上の文字列が「ON」「OFF」と切り替わるようにしていきます。その変更がこちらになります。

import React from 'react'

interface State {
  isToggleOn: boolean
}

export default class Toggle extends React.Component<{}, State> {
  state: State = {
    isToggleOn: false
  }

  toggleState = () => {
    this.setState({
      isToggleOn: !this.state.isToggleOn
    })
  }

  render() {
    const currentState: string = this.state.isToggleOn ? 'ON': 'OFF'
    return (
      <div>
        <p>{currentState}</p>
        <button onClick={this.toggleState}>Toggle</button>
      </div>
    )
  }
}

変更点は、buttonタグにonClick={this.toggleState}、toggleStateメソッドを追加しました。状態更新はthis.setState({...})で行っていて、setState内で状態キー(ここではisToggleOn)の値をtrue, falseで逆転させています。
これにより、Toggleボタンを押すたびに状態が「ON」「OFF」に切り替わるようになりました。

ちなみに、このtoggleStateメソッドではアロー関数を使用しています。renderと同じくクラスメソッドにすることも可能ですが、その際以下のいずれかにする必要があります。

  • onClickの書き方をonClick={this.toggleState}のままにしておきたい場合
    記述で変更ない箇所は省略しています。ここではconstructerメソッドを追加し、その中でbindを使ってクラスメソッドと紐づけるようにしています。
...  

export default class Toggle extends React.Component<{}, State> {
  ...

  constructor(props: any) {
    super(props)
    this.toggleState = this.toggleState.bind(this)
  }

  toggleState() {
    this.setState({
      isToggleOn: !this.state.isToggleOn
    })
  }

  ...
}
  • onClickの書き方を変えても良い場合
    buttonタグのイベントをonClick={() => {this.toggleState()}}にしています。アロー関数が入っていますが、要はやっていることは先のtoggleStateで使っていたアロー関数をonClick内で行ったものになります。
...  
  
export default class Toggle extends React.Component<{}, State> {
  ...

  toggleState() {
    this.setState({
      isToggleOn: !this.state.isToggleOn
    })
  }

  render() {
    const currentState: string = this.state.isToggleOn ? 'ON': 'OFF'
    return (
      <div>
        <p>{currentState}</p>
        <button onClick={() => {this.toggleState()}}>Toggle</button>
      </div>
    )
  }
}  

この辺りの使い分けは人によりますが、私は最初のアロー関数のままか、onClick内の記述が少し面倒になるものの後者が良いかなと思いました。都度メソッドが増えるたびconstructorに記述を追加する方が煩雑かなと思ったので。

4. 親コンポーネントより受け取るプロパティ (Props)

3では状態の扱いについて触れましたが、そのコンポーネントを外部から干渉する必要も往々にしてあります。例えばボタンコンポーネントだと配置する箇所に応じてボタンの色や文字色、サイズも異なってきます。見出しコンポーネントを作るにしても、見出しの内容は内部に閉じているとコンポーネントの柔軟性も損なわれるのでそれを読み込むコンポーネント側からパラメータを送ってやる必要があります。Reactではそうした読み込む(親)コンポーネントから子に渡すpropsというプロパティがあります。Propsと複数形になっているのは、複数のプロパティで渡されることもあるからですね。

では、先のToggleコンポーネントにpropsを追加してみましょう。親コンポーネントからnoteという名前で文字列を受け取り、ボタン下に注記として表示します。
Appコンポーネントでは、Toggleコンポーネントを呼び出すところにnote="(文字列)"を追加します。

import React from 'react'

import Toggle from './components/Toggle'

const App = () => {
  return (
    <Toggle note="※プロフィール設定をロックします" />
  )
}

export default App  

そして、Toggleコンポーネントではpropsで受け取ったnoteの値をもとに表示するよう更新しました。

import React from 'react'

interface Props {
  note?: string
}

interface State {
  isToggleOn: boolean
}

export default class Toggle extends React.Component<Props, State> {
  state: State = {
    isToggleOn: false
  }

  toggleState = () => {
    this.setState({
      isToggleOn: !this.state.isToggleOn
    })
  }

  render() {
    const currentState: string = this.state.isToggleOn ? 'ON': 'OFF'
    return (
      <div>
        <p>{currentState}</p>
        <button onClick={this.toggleState}>Toggle</button>
        { this.props.note && <p>{this.props.note}</p>}
      </div>
    )
  }
}

以下の変更点を行っています。
- interfaceにPropsを追加し、React.Componentに受け渡す
- buttonタグ下でnoteを判定し、文字列があった場合pタグで出力

stateと同じ要領で、propsもthis.props.[props内のキー]で値を参照することができます。一点、interfaceのPropsでnoteキーの後ろに?がついています。これは、noteというキーを「オプショナルで指定する」という意味になります。

www.typescriptlang.org

これをつけていないと、TypeScript側でnoteはあるもの前提でソースコードをチェックするので、もしApp.tsxで呼び出しているToggleコンポーネントにnoteプロパティの記述がなければエラーが表示されてしまいます。

オプショナルの?を外す代わりに、Propsにデフォルト値を指定できるdefaultPropsを指定したらこの問題も解消されます。

import React from 'react'

interface Props {
  note: string
}

interface State {
  isToggleOn: boolean
}

export default class Toggle extends React.Component<Props, State> {
  state: State = {
    isToggleOn: false
  }

  static defaultProps: Props = {
    note: ''
  }

  toggleState = () => {
    this.setState({
      isToggleOn: !this.state.isToggleOn
    })
  }

  render() {
    const currentState: string = this.state.isToggleOn ? 'ON': 'OFF'
    return (
      <div>
        <p>{currentState}</p>
        <button onClick={this.toggleState}>Toggle</button>
        { this.props.note && <p>{this.props.note}</p>}
      </div>
    )
  }
}

qiita.com

5. エレメント取得 (Refs)

素のJSだと、document.getElementById('xxx')等でDOM要素を取得していました。それでボタンクリックのイベントを追加したり要素に文字列等を挿入していましたが、Reactでは先のサンプルのようにDOM取得を行う機会があまりないでしょう。

とはいえ、canvasではgetContextの前に要素を取得するし、ライブラリを組み合わせる際に要素を取得する必要が出てくるので全くないわけではありません。ReactのライフサイクルフックcomponentDidMountのタイミングで、コンポーネントが画面に描画されるのでそのタイミングで、document.getElementById('xxx')等で取得するのでも良いですが、ReactのRefsを使ってDOMを参照することができます。具体的には、取得したい要素にref属性を付与します。それでは、例としてcanvasによる要素取得をrefで試してみましょう。

まず、App.tsxではToggleコンポーネントを使っていましたが、Canvasコンポーネントを読み込み出力するよう変更しました。

import React from 'react'

import Canvas from './components/Canvas'

const App = () => {
  return (
    <Canvas />
  )
}

export default App

そして、Canvasコンポーネントを以下のように記述します。React.createRef()でRefsを作成してcanvasというクラス変数に代入、その後currentにアクセスすることで取得できるようになります。ここではcanvasの描画エリアを400px x 400pxに設定し、その中で100px x 100pxの黒い四角形を描画しています。

import React from 'react'

export default class Canvas extends React.Component {
  canvas = React.createRef<HTMLCanvasElement>()
  cvs: HTMLCanvasElement 
  ctx: CanvasRenderingContext2D

  componentDidMount() {
    this.initCanvas()
  }

  initCanvas() {
    this.cvs = this.canvas.current // get canvas element
    this.cvs.width = 400
    this.cvs.height = 400
    this.ctx = this.cvs.getContext('2d')
    this.ctx.fillStyle = '#000'
    this.ctx.fillRect(150, 150, 100, 100) // draw black rect with 100px * 100px
  }

  render() {
    return (
      <canvas ref={this.canvas}></canvas>
    )
  }
}

reactjs.org

tech-1natsu.hatenablog.com

振り返り

主だったReactの構文をまとめてみました。TypeScriptを使っていることからinterfaceや変数で型定義を行っているためシンプルにReactだけで書くものよりちょっと細かい記述が増えています。その辺りTypeScriptで書くときには留意する必要がありますね。。