Reactでアプリケーション開発(ReactとTypeScriptの知識編)

Reactでアプリケーション開発

Reactは公式ドキュメントを確認してTodoアプリを作ったりしましたが、いざ開発するとなると何からして良いか分からなくなりました。知っていないとならない基礎知識が相当あって全体を把握していないと難しいという印象です。
Reactの書き方だけでなく、JavascriptのES6の機能も把握しておく必要があり、フック、他のライブラリを使うならその知識も必要になってきます。
アプリケーション開発をするにあたって素のReactだけで作るのは色々と問題に直面するので便利なライブラリを選定して使う必要があります。
TypeScriptを使って開発効率を上げるのが最近のトレンドなのでテンプレートにTypeScriptを使います。よってTypeScriptの知識も必要になります。
とにかくシステム開発を一通りするために書籍を参考にするのが近道だと思います。
「TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発」を一通り読んで自分が理解したことを紹介します。興味があれば書籍を購入することをおすすめします。
まずは小さなアプリケーションを自分で作成してみます。

コンポーネントを追加してみる

const Hello = () => {
  const onClick = () => {
    console.log('Hello')
  }
  const text = 'Hello, React'
  return <div onClick={onClick}>{text}</div>
}
export default Hello

コンポーネントに引数を渡す

const Text = (props: { content: string }) => {
  const { content } = props
  return <p>{content}</p>
}
export default Text
//Helloを使う
import Hello from './components/Hello'
//Messeageを使う
import Message from './components/Message'

function App() {
  return (
    <div className='App'>
        <Hello></Hello>
        <Message content='こんにちは'></Message>
      </header>
    </div>
  )
}

export default App

クラスコンポーネントと関数コンポーネントの2種類ありますがシンプルに記載できる関数コンポーネントが主流のようです。フック機能により関数コンポーネントでステートの管理ができるようになったことが大きいようです。
propsキーワードで引数を宣言します。

propsは親から子にデータを一方通行で渡すので子から親にわたすことは出来ないという制限があります。この制限は不都合なようで、バグを減らすために重要な意味を持つのです。一方通行が保証されているからそちらのルートだけ調べて逆走は無視して良いということになります。
引き渡されたpropsは変更ができません。constで宣言しているので当たり前ですがconstで受けなくても変更出来ない定数です。変更ができない制限はまたそのことが保証されているということです。
変更したい場合はコールバック関数を親からpropsに渡して子で実行することで行なえます。変更箇所はコールバック関数のみなので親のコンポーネントでどのように変更されるか把握できるというわけです。

直接スタイルを定義する

JSXではHTML要素で使われている属性は予約後なので使えません。onclickはonClick、classはclassName、CSSもbackground-colorはbackgroundColorのようにキャメルケースで表現します。

const Text = (props: { content: string }) => {
  const { content } = props
  return <p style={{ padding: '16px', backgroundColor: 'grey' }}>{content}</p>
}
export default Text

ただしスタイルを直接記述すると汎用性が損なわれるのでコンポーネントで設定をまとめてしまうのが良いようです。
複数メンバーで相談して決めていないと、クラス名前がバラバラになってしまったり、フォントの大きさが画面ごとにばらばらになったりすることはよくあります。

他の要素を引数に渡す

属性だけでなく他の要素も引数に渡すことが出来ます。<p>要素を引数として渡すサンプルです。

const Container = (props: { title: string; children: React.ReactElement }) => {
  const { title, children } = props
  return (
    <div style={{ background: 'red' }}>
      <span>{title}</span>
      <div style={{ background: 'blue' }}>{children}</div>
    </div>
  )
}
const Parent = () => {
  return (
    <Container title='Hello'>
      <p>ここが子供要素</p>
    </Container>
  )
}
export default Parent

Reactの型

コンポーネントが受け取る引数の型を定義するのが基本です。型を定義することでその後コンポーネントを実装時に定義していた型と異なる実装をすると即座にチェックしてくれるようになります。

type ContainerProps = {
  title: string
  children: React.ReactNode
}
const Container = (props: ContainerProps) => {
  const { title, children } = props
  return (
    <div style={{ background: 'red' }}>
      <span>{title}</span>
      <div style={{ background: 'blue' }}>{children}</div>
    </div>
  )
}
const Parent = () => {
  return (
    <Container title='Hello'>
      <p>ここが子供要素</p>
    </Container>
  )
}
export default Parent

例えば、上記の<Contaier>titleをミススペルするとエラーになります。

Contextを使ったコンポーネント間のデータの渡し方

Reactは親から子にpropsという引数でデータを一方向に渡すのが基本です。シンプルなルールのため弊害もあります。親->子->孫というコンポーネント構成で親から孫にだけ渡したいデータがある場合、間の子にもデータを渡す必要があります。子は自分では使わない引数を受け取って孫に渡します。もっと高階層になればなるほど面倒になると思いませんか。親から孫にデータを直接渡すことは原則出来ないのです。
解決方法の1つにContextがあります。

import React from 'react'
const TitleContext = React.createContext('') //(1)

//孫
const Title = () => {
  return (
    <TitleContext.Consumer> //(2)
      {(title) => {
        return <h1>{title}</h1>
      }}
    </TitleContext.Consumer>
  )
}

//子
const Header = () => {
  return <Title></Title>
}

//親
const Page = () => {
  const title = 'Hello World'
  return (
    <TitleContext.Provider value={title}> // (3)
      <Header></Header>
    </TitleContext.Provider>
  )
}
export default Page
  1. Contextを作成
  2. Consumerで子要素として関数を指定
  3. Providerでvalueにデータを渡す

上記の記述で子である<Header>コンポーネントは引数を指定しなくとも、孫である <Title>コンポーネントで引数を受け取れます。
後述するフックの機能(useContext)によりもっとシンプルに記述する方法もあります。

Hooks(フック)

フックは関数コンポーネントの内部で状態を管理する事ができます。これによりクラスコンポーネントでないと内部の状態を管理出来なかったが関数コンポーネントでも可能になり、関数コンポーネントですべて書くことが出来るようになった理解です。

useState

内部状態を管理するフックを使ってみます。

const [状態, 更新関数] = useState(初期状態)

useState は返り値に状態と更新関数の配列を返します。更新関数を使って状態を更新します。この更新関数以外で状態を更新することは出来ません。

import { useState } from 'react'

type CounterProps = {
  initialValue: number
}
const Counter = (props: CounterProps) => {
  const { initialValue } = props
  const [count, setCount] = useState(initialValue)
  return (
    <div>
      <p>Count:{count}</p>
      <button
        onClick={() => {
          setCount(count + 1)
        }}>
        +(値版)
      </button>
      <button
        onClick={() => {
          setCount(count - 1)
        }}>
        -
      </button>
      <button
        onClick={() => {
          setCount((prevCount) => prevCount + 1)
        }}>
        +(関数版)
      </button>
    </div>
  )
}
export default Counter

直感的には状態を使って更新関数を実行するのが私は分かりやすいです(値版)。公式のリファレンスは関数型の更新で説明されています(関数版)。引数は前の状態を参照するようです。変数名は揃っていれば何でも良いようです。

useReducer

useReducer は複雑な状態をシンプルに記述することが出来ます。

  • 更新関数(dispatch)にactionと呼ばれるデータを渡す
  • reducer関数は、現在の状態とactionを渡すと次の状態を返す
reducer(現在の状態, action){
    return 次の状態
}
const [現在の状態, dispatch] = useReducer(reducer, 初期状態)
import { useReducer } from 'react'

type Action = 'DECREMENT' | 'INCREMENT' | 'DOUBLE' | 'RESET'
const reducer = (currentCount: number, action: Action) => {
  switch (action) {
    case 'DECREMENT':
      return currentCount - 1
    case 'INCREMENT':
      return currentCount + 1
    case 'DOUBLE':
      return currentCount * 2
    case 'RESET':
      return 0
  }
}
type CounterProps = {
  initialValue: number
}
const Counter = (props: CounterProps) => {
  const { initialValue } = props
  const [count, dispatch] = useReducer(reducer, initialValue)
  return (
    <div>
      <p>Count: {count} </p>
      <button onClick={() => dispatch('DECREMENT')}>-</button>
      <button onClick={() => dispatch('INCREMENT')}>+</button>
      <button onClick={() => dispatch('DOUBLE')}>x2</button>
      <button onClick={() => dispatch('RESET')}>Reset</button>
    </div>
  )
}
export default Counter

これで dispatchaction により状態を更新をする処理がスッキリするようになりました。
reducer 関数を見ればどのような action で状態がどのようになるかも見通しが良くなります。

メモ化

Reactは下記の条件で再描画を行います。

  • propsや内部状態の更新
  • コンポーネント内で参照しているContextの更新
  • 親コンポーネントが再描画

親コンポーネントが再描画されると子コンポーネントも再描画されます。
この再描画を制御する機能のことをメモ化と呼ぶようです。
これによりアプリケーションのパフォーマンスが向上します。
メモ化を使うと親コンポーネントの再描画で関係ない子コンポーネントの再描画を制御出来ます。
使い方は関数コンポーネントをmemo関数でラップするだけです。

import React, { memo, useState } from 'react'
type FizzProps = {
  isFizz: boolean
}
const Fizz = (props: FizzProps) => {
  const { isFizz } = props
  console.log(`Fizzが再描画されました。isFizz=${isFizz}`)
  return <span>{isFizz ? 'Fizz' : ''}</span>
}
type BazzProps = {
  isBazz: boolean
}
const Bazz = memo<BazzProps>((props) => {
  const { isBazz } = props

  console.log(`Bazzが再描画されました。isFizz=${isBazz}`)
  return <span>{isBazz ? 'Bazz' : ''}</span>
})
const Parent = () => {
  const [count, setCount] = useState(1)
  const isFizz = count % 3 === 0
  const isBazz = count % 5 === 0
  console.log(`Parentが再描画されました。count=${count}`)
  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
      <p> {`現在のカウント:${count}`} </p>
      <p>
        <Fizz isFizz={isFizz}></Fizz>
        <Bazz isBazz={isBazz}></Bazz>
      </p>
    </div>
  )
}
export default Parent

Parentcount を更新すると、ParentFizz は再描画されますが BazzisBazz が変わらなければ再描画されません。サンプルコードを動かしてログを確認することでその状況が確認出来ます。

useCallback

先程のメモ化したコンポーネントに関数を渡すようにします。
ソースは冗長なのでgitHubの差分を参照してください。
親の再描画で引数に渡す関数が更新されるためメモ化したコンポーネントであっても更新ありとなって再描画されてしまうようになります。
コンポーネントではなく関数をメモ化するために useCallback を使います。これでコンポーネントの再描画を制御することが出来ました。
ソースは冗長なのでgitHubの差分を参照してください。

useEffect

関数の実行をコンポーネント描画後に実行する事ができます。useEffect がないと状態の更新と再描画を繰り返す無限ループに陥る可能性があります。
このような描画に関係がない処理のことを副作用と呼ぶようです。

  • DOMを手動で変更する
  • ログを出力する
  • データの取得

などが副作用の処理としてあげられます。データの取得は非同期でサーバーからデータを取得するのは定番なのでよく使われるフックだと思います。

import { useState, useEffect } from 'react'
const Title = () => {
  const [count, setCount] = useState(0)
  useEffect(() => {
    document.title = `Youre clicked ${count} times`
  }, [count])
  return (
    <div>
      <p> ${count} 回クリックしました。 </p>
      <button
        onClick={() => {
          setCount(count + 1)
        }}>
        クリック
      </button>
    </div>
  )
}
export default Title

useEffect の第二引数は依存配列で、そこに指定した変数が更新されると関数が実行されます。何も指定しないと初期のコンポーネント描画に一度だけ実行されます。

useContext

Contextから値を参照するフックです。
「Contextを使ったコンポーネント間のデータの渡し方」で useContext を使うともっとスッキリかけます。こちらのほうが直感的にかけるように思います。
ソースは冗長なのでgitHubの差分を参照してください。

refのフック

useStateuseReducer は関数コンポーネントで値を保持しますが状態を更新するたびにコンポーネントが再描画されます。 useRef は2つ機能があり、更新しても再描画しないようにすることと、DOMの参照です。

import { useRef, useState } from 'react'
const RefSample = () => {
  const num = useRef(100)
  const inputEL = useRef<HTMLInputElement | null>(null)
  const [text, setText] = useState('')
  const increment = () => {
    num.current += 1
  }
  const handleClick = () => {
    if (inputEL.current !== null) {
      setText(inputEL.current.value)
    }
  }
  return (
    <div>
      <h2>値参照</h2>
      <p onClick={increment}>num : {num.current} </p>
      <h2>DOM参照</h2>
      <input
        ref={inputEL}
        type='text'></input>
      <button onClick={handleClick}>テキスト表示</button>
      <p>入力した値は: {text} </p>
    </div>
  )
}
export default RefSample

increment 関数をいくら実行してもコンポーネントは描画されないので画面に表示されません。
DOM参照で input を参照し、入力文字列を handleClick 関数でinput要素のvalue値を参照してコンポーネントを再描画しています。合わせて num 変数もクリックした分増加した値が表示されます。
入力文字列が増減するたびにコンポーネントを再描画するのは効率が悪いので useRef でDOM要素を参照しています。値の更新で再描画が必要ない場合に使うとパフォーマンスが上がります。

コンポーネント設計に続く

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

先頭に戻る