Redux でコンポーネントを再利用するために考えていること

こんにちは、 Progate の小口です。 本記事は Progate AdventCalendar 7日目の記事です。

flow.js のバージョンアップデートをした話 - Progate Tech Blog でも紹介した通り、 Progate のフロントエンドでは React + Flux + Flow が使われていますが、個人的な工作では Redux を使うことが多いです。

今日は少し業務から離れて、 React + Redux で書いたコンポーネントをその後も再利用・運用可能にするため気をつけていることを書きます。
Style Guide | Redux など見るともう少し網羅的にまとまっていますが、今回はコンポーネントの再利用性の観点から手軽かつ効果の高いものを挙げてみます。

0. 方針: store への(強い)依存をなくす

Redux アプリケーションで「書き捨て」のようになってしまってメンテナンスできない、ちょっとした修正のたびに書き直しが必要になってしまうコンポーネントが生まれる一番の要因は、 store への依存が強くなりがちなことに思われます。
なので、基本的な方針としてはコンポーネントを store に直接依存させないことを意識します。

以下では、そのための工夫を書きます。

1. Selector を定義する

急いでいると、ついこんなコードを書きがちですが

const mapStateToProps = (state) => ({
  todos: state.todos.data,
})

各コンポーネントが store の詳細に依存してしまい、store の構造の変更が非常にやりづらくなります。

このような処理は reducer の近くで selector 層を置くようにして

const getTodos = (state) => state.todos.data

それを使うようにします。

const mapStateToProps = (state) => ({
  todos: getTodos(state)
})

store の詳細をコンポーネントから隠蔽することで、要件やデータ構造の変更に応じて store の内部構造を柔軟に変更しやすくなります。

2. Action Creator では store の「変更意図」を表す

(または、 Action Creator が「単なる setter 」にならないよう気をつける)

store への操作の詳細が直接コンポーネントに表れないようにします。
これがコンポーネントに表れてしまうと、 store の構造変更に合わせてコンポーネントも合わせて変更する必要があるため、 store の変更が高コストになってしまいます。

簡単のために少し無理やりな例になってしまいますが

// action creator
const updateTodo = (todo) => {
  return {
    type: 'UPDATE_TODO',
    payload: todo
  }
}

const diaplayValidationError = (validationError) => {
  return {
    type: 'DISPLAY_VALIDATION_ERROR',
    payload: validationError
  }
}

// コンポーネント
const EditTodoItem = ({todo, updateTodo, displayValidationError}) => {
  const [updatedTodo, setUpdatedTodo] = useState(todo)
  const save = () => {
    const error = /* バリデーション処理 */

    if (!error) {
      updateTodo(updatedTodo)
    } else {
      displayValidationError(error)
    }
  }

  return (
    <div>
      /* TODO 編集フォーム */
      <button onClick={save}>Save</button>
    </div>
  )
}

この例だと、バリデーションエラー以外に考えることが増えた場合、 store の構造だけでなくコンポーネントも合わせて改修する必要が出てきます。

Action Creator に寄せてみます。

// action creator
const updateTodo = (todo) => {
  const error = /* バリデーション処理 */

  if (!error) {
    return {
      type: 'UPDATE_TODO',
      payload: todo
    }
  } else {
    return {
      type: 'DISPLAY_VALIDATION_ERROR',
      payload: validationError
    }
  }
}

// コンポーネント
const EditTodoItem = ({todo, updateTodo}) => {
  const [updatedTodo, setUpdatedTodo] = useState(todo)
  const save = () => updateTodo(updatedTodo)
  return (
    <div>
      /* TODO 編集フォーム */
      <button onClick={save}>Save</button>
    </div>
  )
}

store を変更する場合 Action Creator は合わせて修正する必要がありますが、コンポーネントはそのまま使いまわせます。

表示の都合などでコンポーネントでエラー状態に関心がある場合は、 selector を使って store から取得するようにします。

最後に

背景が足りないなどうまく伝わらなかったらすみません。

明日は moritanzania さんによる ProgateAdventCalendar 8日目です。お楽しみに!