Reduxの"Normalizing"パターンをTypeScriptで攻略する

# はじめに

Reduxで深いネストを持つstate管理のベストプラクティスの一つに、データを平坦にして(Normalizing)管理する方法がReduxの公式ページで紹介されています。公式ページでは`byId`と`allIds`という２つの変数を使って解説されています。

[Normalizing State Shape](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape)

TypeScriptでの`byId`の型定義やreducerの実装で試行錯誤したことを共有します。

# 対象読者

* Normalizingパターンを知らない人で
  * ネストが深いデータを管理していてパフォーマンスが気になる
  * データのネストが深くてreducerを書くのがしんどい
* Normalizingパターンを知っているけど
  * byIdの型定義方法が分からず、とりあえず`any`にしている
  * reducerでbyIdの操作が難しいと感じる

# Normalizingパターンとは？

## Normalizeされていないデータの問題点

まずは公式ページで紹介されているようにNormalizeされていないデータをご覧ください。

```typescript:Normalize前

[
  {
    id: 'post1',
    author: { username: 'user1', name: 'User 1' },
    body: '......',
    comments: [
      {
        id: 'comment1',
        author: { username: 'user2', name: 'User 2' },
        comment: '.....'
      },
      {
        id: 'comment2',
        author: { username: 'user3', name: 'User 3' },
        comment: '.....'
      }
    ]
  },
  {
    id: 'post2',
    author: { username: 'user2', name: 'User 2' },
    body: '......',
    comments: [
      {
        id: 'comment3',
        author: { username: 'user3', name: 'User 3' },
        comment: '.....'
      },
      {
        id: 'comment4',
        author: { username: 'user1', name: 'User 1' },
        comment: '.....'
      },
      {
        id: 'comment5',
        author: { username: 'user3', name: 'User 3' },
        comment: '.....'
      }
    ]
  }
  // and repeat many times
]
```

このような構造のデータが抱える問題点について、公式ページではざっくり次のように言っています。

* 同じデータが複数の場所にあるケースだとupdateのreducer書くの大変だよね
* そもそもネスト深いとreducer書くの大変だよね
* 木構造の枝データが更新された場合でも、幹側のViewコンポーネント全てに無駄なrender走っちゃうからパフォーマンス悪いよね

## Normalizingパターンが解決してくれること

次に、先ほどのデータをNormalizeしたものをご覧ください。

```typescript:Normalize後
{
    posts : {
        byId : {
            "post1" : {
                id : "post1",
                author : "user1",
                body : "......",
                comments : ["comment1", "comment2"]
            },
            "post2" : {
                id : "post2",
                author : "user2",
                body : "......",
                comments : ["comment3", "comment4", "comment5"]
            }
        },
        allIds : ["post1", "post2"]
    },
    comments : {
        byId : {
            "comment1" : {
                id : "comment1",
                author : "user2",
                comment : ".....",
            },
            "comment2" : {
                id : "comment2",
                author : "user3",
                comment : ".....",
            },
            "comment3" : {
                id : "comment3",
                author : "user3",
                comment : ".....",
            },
            "comment4" : {
                id : "comment4",
                author : "user1",
                comment : ".....",
            },
            "comment5" : {
                id : "comment5",
                author : "user3",
                comment : ".....",
            },
        },
        allIds : ["comment1", "comment2", "comment3", "commment4", "comment5"]
    },
    users : {
        byId : {
            "user1" : {
                username : "user1",
                name : "User 1",
            },
            "user2" : {
                username : "user2",
                name : "User 2",
            },
            "user3" : {
                username : "user3",
                name : "User 3",
            }
        },
        allIds : ["user1", "user2", "user3"]
    }
}
```

Normalizingパターンとは、データをテーブルごとに管理してリレーションを持たせる手法です。より具体的には、`byId`は`id`をkeyにデータを持っています。またデータに順序を持たせる目的で`allIds`がそれぞれのデータの`id`のみを配列で持っています。

公式ページではNormalizingによってもたらされる改善点を４つ挙げています。

* データの実体は１つしか存在しないから更新が簡単です。
* ネストが浅いのでreducerを書くのが簡単です。
* データを探すのに必要な情報は、テーブルとIDの２つだけ。
* データはそれぞれ独立しているから、View側で無駄なrenderが走りません。

## Normalizingパターンの採用について

データの更新が行われない、またはAPIからのデータ取得によってデータが一括で書き換わりreducerが複雑にならないケースではNormalizingパターンは採用しなくてもいいかもしれません。

公式ページでも以下のような例を挙げているので、いつでもNormalizingパターンを使うべきとは考えていないようです。

```:stateでテーブルデータをまとめる親keyは"entities"がオススメらしいです
{
    simpleDomainData1: {....},
    simpleDomainData2: {....},
    entities : {
        entityType1 : {....},
        entityType2 : {....}
    },
    ui : {
        uiSection1 : {....},
        uiSection2 : {....}
    }
}
```

# TypeScriptによるNormalizingパターンのコード例

Normalizingパターンの紹介が済んだところで、TypeScriptでのコード例を示していきます。

## `byId`の型定義

Normalizeしなければ型定義はとてもシンプルです。

以下のコードは、VSCodeの"Paste JSON as Code"というプラグインを使ってNormalize前のデータから型定義を生成しました。

```typescript

// Generated by https://quicktype.io

export interface State {
  blogPosts: BlogPost[];
}

export interface BlogPost {
  id:       string;
  author:   Author;
  body:     string;
  comments: Comment[];
}

export interface Author {
  username: string;
  name:     string;
}

export interface Comment {
  id:      string;
  author:  Author;
  comment: string;
}
```

ではNormalizingパターンで表現されたデータはどうでしょう。

試しに再び"Paste JSON as Code"を使ってみました。

```typescript
// Generated by https://quicktype.io

export interface State {
  posts:    Posts;
  comments: Comments;
  users:    Users;
}

export interface Comments {
  byId:   CommentsByID;
  allIds: string[];
}

export interface CommentsByID {
  comment1: Comment;
  comment2: Comment;
  comment3: Comment;
  comment4: Comment;
  comment5: Comment;
}

export interface Comment {
  id:      string;
  author:  string;
  comment: string;
}

export interface Posts {
  byId:   PostsByID;
  allIds: string[];
}

export interface PostsByID {
  post1: Post;
  post2: Post;
}

export interface Post {
  id:       string;
  author:   string;
  body:     string;
  comments: string[];
}

export interface Users {
  byId:   UsersByID;
  allIds: string[];
}

export interface UsersByID {
  user1: User;
  user2: User;
  user3: User;
}

export interface User {
  username: string;
  name:     string;
}
```

予想はしていましたが`byId`が全然ダメですね。もちろん`〜ById`以外は使えそうです。

`byId`は未知のkeyを持ちますが、そのvalueの型は決まっています。以下のように型定義できます。

```typescript

export interface State {
  entities: {
    posts: Posts;
    comments: Comments;
    users: Users;
  };
}

export interface Comments {
  byId: CommentsByID;
  allIds: string[];
}

export interface CommentsByID {
　[Key: string]: Comment;
}

export interface Comment {
  id: string;
  author: string;
  comment: string;
}

export interface Posts {
  byId: PostsByID;
  allIds: string[];
}

export interface PostsByID {
　[Key: string]: Post;
}

export interface Post {
  id: string;
  author: string;
  body: string;
  comments: string[];
}

export interface Users {
  byId: UsersByID;
  allIds: string[];
}

export interface UsersByID {
　[Key: string]: User;
}

export interface User {
  username: string;
  name: string;
}
```

byIdの型定義の方法が分からず調べたことがこの記事のモチベーションになっています。（達成感）

## reducerでbyIdの操作

ポイントになる部分のみ紹介します。

javascriptだけで書けないこともないのですが、[Reduxの公式ページ](https://redux.js.org/recipes/structuring-reducers/updating-normalized-data)でもlodashの`merge`を最初に紹介しているので、私は出来るだけlodashを使う書き方に寄せています。

Redux公式ページではいろんなライブラリーを使った書き方が紹介されていたり、immutable.jsを使うことの功罪など詳しく書かれています。ぜひそちらも読んでいただいて、私の以下のコードはあくまで１つの参考になれば幸いです。

### 配列 → `byId`の変換

lodashを使う例

```typescript

const byId = _.keyBy(comments, "id");
```

javascriptだけの例

```typescript

// acc = accumulator
const byId = comments.reduce(
  (acc, comment) => ({ ...acc, [comment.id]: comment }),
  {}
);
```

この例だと読みやすさはlodashの圧勝ですね。

### あるpostのbodyの編集

lodashを使う例

```typescript

const updateBody = (state: Posts, id: string, body: string): Posts => {
  return {
    ...state,
    byId: _.mapValues(state.byId, post => {
      if (post.id !== id) { return post; }
      return { ...post, body };
    })
  };
};
```

javascriptだけの例

```typescript

const updateBody = (state: Posts, id: string, body: string): Posts => {
  return {
    ...state,
    byId: {
      ...state.byId,
      [id]: { ...state.byId[id], body },
    },
  };
};

```

レコード１件だけの更新だとjavascriptだけの書き方が読みやすそうです。
lodashのmapValuesを使う書き方だと、全件更新するケースでも書き方はあまり変わりません。

NormalizingパターンをTypeScriptで書く際のTipsは以上です。
どなたかの参考になれば幸いです。