はじめてのReact

2020 T.Takeda

React↗ とは?

JavaScript(ES)のUIフレームワークの一つ

  • 特徴として、状態に応じて適切なコンポーネントを効率的に描画する仕組みを持つ
  • 大事なことは公式ドキュメントに書いてある

JavaScriptのおさらい

はじめてのJavaScript

  • スクリプト言語
  • インタプリタ
  • プロトタイプベースのオブジェクト指向
  • 動的型付け、弱い型付け
  • チェーン、スコープ、クロージャー
  • シングルスレッド
  • GC

JavaScriptの台頭

事実上のWebデファクトスタンダード

1. IE/Netscapeブラウザ2強時代
2. JavaScript/VBScriptの共存時代
3. スクリプト無効化時代
4. MS ActiveX/Adobe Flashの終焉とともにJavaScript一強時代

JavaScriptの扱いづらい点

  • 構造化されていない
  • プロトタイプチェーン
  • 動的型付け、弱い型付け
  • 2パス実行でスコープが特殊
  • ブラウザ互換性、ES互換性
  • サーバーサイドJS or クライアントサイドJS
  • 非同期呼び出し
  • コールバック地獄
  • 安易なグローバル変数
  • 開発環境管理が大変
💡ネイティブなままでは品質の担保が難しい

Webブラウザの発展と功罪

多様な規格/フレームワークを生み出してきた

  • HTML5
  • CSS
  • Node.js, Angular, Vue.js, jQuery
  • npm/npx/yarn
  • Babel
  • TypeScript
  • Promise
  • Flux/Redux
  • React

そして、こうなった

Node.js + npx + Babel + TypeScript + Promise + ... + React

React(+Redux)の特徴

Why Use React Redux↗
  • 宣言的(関数型パラダイム)
  • コンポーネント指向
  • 一方向データフロー
  • Virtual DOMとJSX

宣言的

  • 参照透過性を極力保つ = 副作用を適切に扱う
    • 関数内部で外部の値を書き換えない → 純粋関数
    • 同じ引数なら同じ返値 → テスト容易性
    • 評価順序に依存しない → 遅延評価可能性
  • 純粋関数は関数合成でき、高階関数になれる
    • JavaScriptが元々持っている性質
    • 無名関数
    • カリー化 → map/filter/reducer

関数型パラダイム

  • Functional Reactive Programmingはあえて採用していない様子
    • 時系列的なイベントハンドリングがない
    • 学習コストを下げるため
    • Flux/Reduxでカバーできている
  • 副作用は多数残る
    • DOM/API/console.log/ファイル/プロトタイプチェーン

コンポーネント指向

Passing Props to a Component↗
  • 親子関係
  • 親から子への一方向性
  • propsはイミュータブル(書き換え不可)
  • 関数コンポーネント
💡かつてはクラスコンポーネントが存在していた
💡今後は関数コンポーネントを使う

関数コンポーネントの例

const NumberList = (props) => {
  const numbers = props.numbers
  const listItems = numbers.map((number) => <li>{number}</li>)
  return (
    <ul>{listItems}</ul>
  )
}

const numbers = [1, 2, 3, 4, 5]
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<NumberList numbers={numbers} />)

一方向データフロー↗

  • コンポーネントは親から子へのツリー構造
  • 子は親から渡されたpropsを参照可能

親コンポーネントから子コンポーネントへのprops渡し

💡親コンポーネントのDOMの外側に子コンポーネントを描画することもできることはできる(portal/bubbling)

DOM

Document Object Model

  • オブジェクト指向
  • Webページを表現するモデルとAPI↗
  • JavaScriptからDOMのAPIを操作する

Virtual DOM

  • DOMを直接操作したり更新するのは高コスト
  • Reactは、Virtual DOMをメモリ上で保持
    • 実態はDOMを表現したJavaScript
  • 差分検知して更新
    • 書き方が悪いと常に全体を更新する懸念もある

JSX

  • 独自のDSL(埋め込み言語)
    • テンプレートエンジンではない
  • HTMLっぽくJavaScriptを記述できる
  • 変更があるとrender()でVirtual DOMを効率的に更新
  • ReactがReal DOMを一致させる
💡元々ReactではJSXで書くことが一般的だった

JSXの例

const Example = () => {
  const [count, setCount] = useState(0)
  return (
    <div>
    <p>You clicked {count} times</p>
    <button onClick={() => setCount(count+1)}>
      Click me
    </button>
    </div>
  )
}
  • HTMLっぽいものがJavaScriptに埋め込まれている
    • count変数が参照されており、値が更新されると再描画される
    • クリック時の処理が無名関数で書かれている

コンポーネントの考え方

Components/Containers

  • Components
    • 状態に依存しない純粋なコンポーネント群
    • = Propsが同じなら、同じRendering結果を返す
    • いわゆるプレゼンテーション層
  • Containers
    • 状態を持つコンテナー群
    • ビジネスロジックを持ち、値を保持したり、状態の変更をトリガーしたりする

Components

  • ReactではほぼすべてがComponentsである
  • 状態を持たないComponentsを内部に持つことはあるが、Containersを内部に持つことはない

Componentsの階層構造

Containers

  • コンテナ自身は状態を持つ
  • 状態を持たないComponentsを内部に含むことは一般的
  • 状態とコンポーネントの分離のための結合点

Containersの階層構造

Componentsが持つ状態

  • Componentsが状態を持たないわけではない
  • Componentsの責務に応じて必要な状態を持つ

Componentsが持つ状態の例

Componentsの責務を明確に

  • 何を受け取って、何を表示するのか?
(props) => render
  • 型を見ると関数の役割がわかるように明快に
sum :: [Int] -> Int
max :: Ord a => [a] -> Int
length :: [a] => Int
map :: (a -> b) -> [a] -> [b]
all :: (a -> Bool) -> [a] -> Bool

Components設計のヒント

  • Atomic Design
    • 主にWebデザイン向けの設計方法
    • 厳密に従うのは難しいが、ひとつの指標として有用

Atomic Design

Pages ひとつのページ全体
Templates ページ構成の基本となる型
Organisms コンポーネントの組み合わせ
Molecules 複合することで意味を持つコンポーネント
Atoms 最小単位のコンポーネント
💡Organisms/Molecules/Atomsは純粋コンポーネントであることが理想的

コンポーネントは小さく保つ

  • ロジックの分離
  • 役割の明確化
  • 再利用性
  • テスト容易性
💡品質のために自明な性質にする

説明用コンポーネントの構成

説明用コンポーネント

  • 単一のJSX実装の場合
    • 本来無関係なコンポーネントも含めて密結合になる
    • 一部の修正が他のコンポーネントにも影響する
const Page = (
  <>
    <Collapse><p>...</p></Collapse>
    <Collapse><p>...</p></Collapse>
    <Collapse><p>...</p></Collapse>
    <Modal>
      <Modal.Header>
        <strong>Title</strong>
      </Modal.Header>
      <Modal.Body>
        <h4>Some messages</h4>
      </Modal.Body>
      <Modal.Footer>
        <Button>...</Button>
        <Button>...</Button>
      </Modal.Footer>
    </Modal>
  </>
)

コンポーネントを小さくする

コンポーネントを小さく

const ModalHeader = () => (
  <Modal.Header>
  <strong>Title</strong>
  </Modal.Header>
)
const ModalBody = () => (
  <Modal.Body>
    <h4>Some messages</h4>
  </Modal.Body>
)

コンポーネントを階層化

コンポーネントを階層化する

const ButtonCancel = () => <Button>...</Button>
const ButtonConfirm = () => <Button>...</Button>

const ModalFooter = () => (
  <Modal.Footer>
    <ButtonCancel />
    <ButtonConfirm />
  </Modal.Footer>
)

小さなコンポーネントで構成する

小さなコンポーネントで構成する

const ConfirmationModal = () => (
  <Modal>
    <ModalHeader />
    <ModalBody />
    <ModalFooter />
  </Modal>
)

ページ構成も小さく保つ

小さなコンポーネントでページを構成

ページ構成も小さく保つ

const CollapseStep1 = () => (
  <Collapse><p>...</p></Collapse>
)
const CollapseStep2 = () => (
  <Collapse><p>...</p></Collapse>
)
const CollapseStep3 = () => (
  <Collapse><p>...</p></Collapse>
)

const Page = (
  <>
    <CollapseStep1 />
    <CollapseStep2 />
    <CollapseStep3 />
    <ConfirmationModal />
  </>
)

状態の持ち方とProps渡し

  • コンポーネントの入力はPropsとして渡す
    • コンポーネントを拡張して状態を持たせない

状態の持たせ方

💡状態をどこに持たせることが適切かは常に考える

styled-components↗

  • CSSを直接コード中に記述できる
import Button from 'react-bootstrap/Button'
import styled from 'styled-components'

const StyledButton = styled(Button)`
  width: 50%;
  height: 100%;
`

Redux

Redux↗

  • 一方向データフローにより状態管理を一元化する
  • コンポーネント間のやりとりを安全に行う
  • Flux拡張

JavaScriptでの状態管理

  • グローバル変数
    • どこからでも変更、読み出し可能
    • 影響範囲が不明
    • 構造化が難しい

JavaScriptで安全に状態を管理するためにどうするか?

  1. グローバル変数は使う
  2. 変数の直接的な変更を禁止
  3. 状態を変更するための純粋関数を通す
  4. 状態が変更された場合に呼び出し元に通知

安全な状態管理

Reduxでの状態管理の基本形

Reduxでの状態管理の基本形

three-principles↗

Reduxでの状態管理

Reduxでの状態管理

図引用:redux.js.org↗

Reduxでの状態管理

Reduxでの状態管理

Redux - Dispatch

  • Stateを更新するための唯一の手段
  • どのアクションを実行するか引数で渡す

Reduxでの状態管理

Redux - Actions

  • 何をするか知っているイベント
  • ペイロードに値を持つ
  • DispatchによりReducerにペイロードの値を渡して処理してもらう

Reduxでの状態管理

Redux - Reducers

  • 現在のstateとactionsから新しいstateを作る
    • actionに対するイベントリスナーのようなもの
  • 純粋関数である
    • 副作用を持たない、非同期に処理しない
    • 既存のstateを変更することはない

Reduxでの状態管理

Redux - Store

  • 必ず1つだけ
  • 現在のstateを保持する
  • stateはread-only、getStateで取得可能
  • reducerで生成される

Reduxでの状態管理

Redux - Selectors

  • storeの規模が大きくなるとstateからの値の取り出しが冗長になる
  • 選択的に値を取り出せるようにするヘルパー

Reduxでの状態管理

React Hooks

React Hooks

  • 関数コンポーネントからのみ呼び出せる
    • クラスコンポーネントと共存できない
  • トップレベルでのみ使える
    • if/for/callbackなどでは使えない
  • Hooksの登場によりReactスタイルが変わった
    • Reduxが無用の長物になっている状況では、React Hooksで軽量に記述できる

React Hooks

  • useState
    • 新しいstateとstateを更新するための関数を定義できる
    • 初期値を渡せる
    • 通常は単一の値を保持する
  • useEffect
    • 指定した値が更新されたときに行いたい処理を呼び出すことができる
    • 遅延評価
  • useReducer
    • 純粋関数で値を更新できる

useState

const Example = () => {
  const [count, setCount] = useState(0)
  return (
    <div>
    <p>You clicked {count} times</p>
    <button onClick={() => setCount(count+1)}>
      Click me
    </button>
    </div>
  )
}
💡useStateだけで状態管理できる
💡ReduxでDispatch/Action/Reducer/Storeを用意して、this.state.countを使わなくてよくなった

useState

複数の状態を単一のStateで管理するときは、その方法が適切かを考える

  • オブジェクトで構造化した場合
    • 取り得る状態の組み合わせを制限できる
  • 独立した変数とした場合
    • 非同期に更新されるため、すべての状態の組み合わせを許容する設計にする必要がある

オブジェクトで構造化した場合

  • 取り得る状態を単一のSetterで管理できる
    • 整合性の担保はアプリケーションの責務
    • 状態の組み合わせが不正にならないように設計する
const [signInFlow, setSignInFlow] = useState({
    isAuthenticated: false,
    isLocked: false,
    isTemporaryPassword: false,
    isError: false
})

独立した変数とした場合

  • 個別のSetterで独立に更新できる
  • 状態の組み合わせがすべて起こりえるため、許容した設計とする
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [isLocked, setIsLocked] = useState(false)
const [isTemporaryPassword, setIsTemporaryPassword] = useState(false)
const [isError, setIsError] = useState(false)

setStateの評価コスト

  • setStateを呼び出した時点ではすぐに値は更新されない
    • 次のレンダリングサイクルで更新される
  • setした値が前回と同じであっても再評価されるため計算コストはかかる
  • どこでも使えてシンプルな回避策
// 値が変化しない場合は呼び出さない
if (currentState !== nextState) {
  setCurrentState(nextState)
}

useEffect

useEffect(() => {
  document.title = `You clicked ${count} times`
})
  • 副作用をReactiveに反映できる

useEffect

useEffect(() => {
  ChatAPI.subscribeFriendsStatus()
  return () => {
    ChatAPI.unsubscribeFriendsStatus()
  }
})
💡マウント時にsubscribeして、アンマウント時にunsubscribeすることができる

useEffect

  • 依存を明確に記述する
useEffect(() => {
  document.title = `You clicked ${count} times`
}, [count])
💡countが変更された場合にのみ再評価

useEffectの評価タイミング

  • 画面をレンダリングした後に評価される
    • 依存に変化があったときに再評価される
    • コンポーネント全体が再評価されるときにも評価される
useEffect(() => {
  const filtered = items.filter(isSatisfied(deposit))
}, [items, deposit])

Propsの評価タイミング

  • 親コンポーネントから渡されたpropsが変化するとコンポーネントは再描画され、変数は再評価される
// items/depositがpropsのとき、変化する度に再評価される
const filtered = items.filter(isSatisfied(deposit))
💡useEffectとProps渡しの違いは?

評価タイミングの違い

  • useEffectはレンダリングとは無関係に、依存が変化したときに非同期的に評価される
    • レンダリング後にも評価される
  • 計算コストが高くなく、同期的で副作用がないものは単なる変数でよい
  • レンダリングに影響を与える計算コストの場合、非同期、副作用を伴うものはuseEffectの使用が適切

依存を適切に書く

  • 依存漏れ
  • 依存間違い
  • 値は異なるが、オブジェクトは同値
  • 値は同じだが、オブジェクトは異なる

依存漏れ

本来、再評価が必要な依存が漏れているため、値が更新されない、古い値を参照するなど意図しない動作を引き起こす

useEffect(() => {
  getSomething(myId)
}, [])
  • myIdが最後に評価されたときの値のまま変わらないため、おかしな挙動になる可能性がある

依存間違い

無関係な値の更新時に再評価され、計算コストだけがかかる

useEffect(() => {
  getSomething(myId)
}, [workspaceId])
  • 本来依存のない値の更新タイミングに依存して、適切に動いているように見えて、他の適切でない依存関係の誤りに気づけない懸念がある

値は異なるが、オブジェクトは同値

  • 依存にObjectやArrayを指定する場合に注意
    • 依存漏れと同じ
useEffect(() => {
  arrayCorpIds.map(corpId => {
    getSomething(corpId)
  })
}, [arrayCorpIds])
  • arrayCorpIdsの配列の中身が更新されたにも関わらず、arrayCorpIdsはオブジェクトとしては同値のため、再評価されず

値は同じだが、オブジェクトは異なる

  • 同じ値を保持しているオブジェクトを再生成した場合
    • 毎回再評価されて無駄な計算コストが掛かる
useEffect(() => {
  arrayCorpIds.map(corpId => {
    getSomething(corpId)
  })
}, [arrayCorpIds])
  • arrayCorpIdsの配列の中身が変わらないのに、arrayCorpIdsはオブジェクトとしては異なるため、再評価される

無限ループを回避する

  • useEffectが依存する値をuseEffectの中で書き換えると起こる
  • シンプルな対策としては、ガードする
    • どこでも使えて一番シンプルな回避方法
    • オブジェクト、配列の場合は比較が高コストなので注意
if (currentState !== nextState) {
  setCurrentState(nextState)
}

useReducer

  • 複雑な状態管理ロジックをreducerに閉じることができる
type State = {
  isAuthenticated: boolean
  isLocked: boolean
  isTemporaryPassword: boolean
  isError: boolean
}
  
type Action =
  | { type: 'authenticate' }
  | { type: 'lock' }
  | { type: 'setTemporaryPassword' }
  | { type: 'setError' }
  | { type: 'reset' }

useReducer

  • Reducerは純粋関数にできる
    • 状態に整合性が必要な場合はここで担保できる
const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'authenticate':
      return { ...state, isAuthenticated: true }
    case 'lock':
      return { ...state, isLocked: true }
    case 'setTemporaryPassword':
      return { ...state, isTemporaryPassword: true }
    case 'setError':
      return { ...state, isError: true }
    case 'reset':
      return { isAuthenticated: false, isLocked: false,
            isTemporaryPassword: false, isError: false }
    default:
      return state
  }
}

useReducer

  • 使う側はユーザー操作と対応するアクションとして記述できる
// 認証完了時に呼び出す
dispatch({ type: 'authenticate' })

// ロック状態だったときに呼び出す
dispatch({ type: 'lock' })

// 仮パスワード状態だったときに呼び出す
dispatch({ type: 'setTemporaryPassword' })

// エラー時に呼び出す
dispatch({ type: 'setError' })

// サインアウトに呼び出す
dispatch({ type: 'reset' })

useStateかuseReducerか

  • useStateの場合
    • 複数定義してもすべて独立した個別の状態管理
    • 状態が複雑な場合にロジックが分散する懸念
    • 単一の状態管理、局所的な状態管理に最適
  • useReducerの場合
    • 状態管理のロジックをReducerに閉じられる
    • オブジェクト構造の状態の整合性をとる
    • ユーザー操作のアクションで状態が変わることを記述できる

memo/useMemo

  • 値が変わらない限り再レンダリングする必要がないものをメモ化する
例)ログイン後の名前表示、言語切り替えで変わる文言、など
  • memo化にもコストがある
    • memo化するということは、常に評価結果を保持し、同値かどうかを評価する必要がある
💡計算コストの方が低い場合はmemo化しない

useLayoutEffect

  • 画面をレンダリングする前に評価される
    • 描画をブロッキングする
    • 一瞬現れるちらつきを抑制できる

derived state

propsから導出された変数をどう扱うか

  • useEffectに入れる?
    • 親コンポーネントがレンダリングされるたびにuseEffectは評価されるため、useEffectで計算させると無駄な再レンダリングが非同期に走る可能性がある
  • useStateに入れる?
    • 必ずしもstateに格納する必要はない可能性がある
    • 初期レンダリングを初期値で行わせ、レンダリング中にsetStateで変更し再レンダリングさせることはできる

derived state

  • 単なる変数として宣言する
    • 多くの場合、単に変数として導出したいことが多い
    • 演算コストが低いなら毎回計算させた方がよい
  • useMemoに入れる
    • 再計算を抑制することはできる
    • 演算コストを抑えることができるが、比較コストはある
    • 単なる変数だが再レンダリングのたびに計算させるにはコストが高い場合に有効
  • useState/useEffect
    • 副作用的に値が変更される場合には使う

Context

  • Props渡しリレーを回避するための「グローバル」な機構
    • 多用するとグローバル変数を遠回しに使っているだけの状況になるので、純粋なJavaScriptを書いた方がよくなる
💡認証情報、言語設定、ダークモードなどグローバルにどのページでも利用するものに限定して利用する

useContext

  • Providerで値をグローバルに共有できる
    • Props伝搬地獄を取り除ける
  • 他方で、コンポーネント間に容易に依存を生む

React Custom Hooks

  • Hooksは自前で実装できる
    • useStateなどは特殊なものではなく同じものを実装できる

React Custom Hooks

  • useContextとの違いは?

useCustomHook

  • useCustomHookは単にカプセル化しただけなので、複製されている

useCustomHook

useContext

  • useContextはグローバルに一つのコンテキスト

useContext

Redux Custom Hooks

Redux独自のカスタムフック拡張

  • useSelector
    • Selectorで指定する値に変更があったら自動的に再描画してくれる
  • useDispatch
    • 渡されたactionでdispatchする

どの組み合わせもあり得る

  • React
  • React + React Hooks
  • React + Redux
  • React + React Hooks + Redux
💡どれでも同じようなことを実現できる
💡チームの場合は方針やルール決めが大事

そして、こうなる

React + React Hooks + Redux + JSX + TypeScript + Promise + Redux-Saga + react-bootstrap + Babel + Webpack + terser + Node.js + npx + ESLint + create-react-app + ...

💡GraphQL使うならapollo-clientやamplifyなんかも・・

APPENDIX

TypeScript↗

  • 型安全
    • カリー=ハワード同型対応
  • 限界はある
    • anyを使わざるを得ない・・
  • 型付け苦行

Babel↗

  • 最新のESに対応するためのモジュール
  • babelify
    • 最新のESをネイティブなESに落とし込む
  • JSX with babel

Browserify↗

  • サーバーサイドJSをクライアントサイドJSに変換する
    • Node.jsをブラウザで動かす
    • npmモジュールを取り込める(requireはブラウザにない)

Webpack↗

  • bundle.jsとして、諸々のアセットをパッキング

react-bootstrap↗

  • UIコンポーネント
  • BootstrapをReactコンポーネント化したもの

Redux-Saga↗

  • 副作用を上手に扱うためのモジュール
  • 非同期処理/並列処理ミドルウェア
    • 使うだけならすぐ使える
    • 処理を理解するにはES6のiterator/generatorを理解する必要がある
  • Actionsと紐付くことで、stateを更新できる

create-react-app↗

  • React環境をコマンド一発で構築できるモジュール

Promise

  • await/asyncスタイル使いましょう

よりよいReactライフを!