【MUI】Data Gridのソートをカスタムする

この記事は [NTTコムウェア Advent Calender 2024](https://qiita.com/advent-calendar/2024/nttcomware) 20日目の記事です。

## はじめに
NTTコムウェアの中原 です。
最近は [SmartCloud® オーケストレータ](https://sc.nttcom.co.jp/orchestrator/) の開発チームで React の MUI (Material-UI) を触っております。

表にデータを出力したいとき、とりあえず [Data Grid](https://mui.com/x/react-data-grid/) を使うことが多いのですが、**ソート機能のカスタマイズ**が少し難しかったので備忘録として整理してみます。
本記事は [公式ドキュメント](https://mui.com/x/react-data-grid/sorting/) と重複する部分もありますので、そちらもご覧ください。

## ソートの基本
配列におけるソートの基本的な仕組みは、要素を2つ取り出して値の比較ですね。
JavaScript の ``sort()`` を用いた簡単な例を示します。
返り値がゼロ、正、負のいずれであるかによって処理が変化します。

### 昇順
```js
const numbers = [5, 2, 9, 1, 5, 6]
numbers.sort((v1, v2) => v1 - v2) // ここの返り値が重要

/*
2 と 5 を比較： 2 - 5 = -3 （2を5の左へ）
9 と 6 を比較： 9 - 6 = 3 （6を9の左へ）
5 と 5 を比較： 5 - 5 = 0 （変更なし）
...
*/

console.log(numbers) // [1, 2, 5, 5, 6, 9]
```
### 降順
```js
const numbers = [5, 2, 9, 1, 5, 6]
numbers.sort((v1, v2) => v2 - v1) // ここの返り値が重要

console.log(numbers) // [9, 6, 5, 5, 2, 1]
```

## サンプルの準備
適当なオブジェクト配列 ``rows`` と ``columns`` を用意し、Data Grid に出力できたら準備完了です。
``x-data-grid`` のバージョンは ``7.6.1`` です。
```react
import React from 'react'
import Box from '@mui/material/Box'
import { DataGrid } from '@mui/x-data-grid'

const rows = [
    { id: 1, name: 'Apple', price: 1000, stock: 20 },
    { id: 2, name: 'Orange', price: 2000, stock: 15 },
    { id: 3, name: 'Peach', price: 1500, stock: 10 },
    { id: 4, name: 'Banana', price: 500, stock: 30 },
    { id: 5, name: 'Grape', price: 3000, stock: 5 },
    { id: 6, name: 'Lemon', price: 2500, stock: 8 }
]

export default function App() {
    const columns = [
        { field: 'id', headerName: 'ID' },
        { field: 'name', headerName: 'Name'},
        { field: 'price', headerName: 'Price'},
        { field: 'stock', headerName: 'Stock'}
    ]
    
    return (
        <Box>
            <DataGrid
                rows={rows}
                columns={columns}
            />
        </Box>
    )
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945817/84b3e5c0-6099-5157-1d95-8cf9c7595c2e.png)


## ``sortComparator`` の基本
 ``sort()`` と同じ要領で、昇順・降順のソートを実装します。
``columns`` の要素に ``sortComparator`` プロパティを追加し、引数 ``(v1,v2)`` と返り値 ``v1 - v2`` を書きます。
これだけで**昇順・降順の両方**をカバーしています。
返り値 ``v1 - v2`` より、昇順にしかならないようにも見えますが、**降順の場合は返り値の符号が反転する**よう Data Grid が処理します。
（人によっては余計なお世話かも？）

```js
const columns = [
    { field: 'id', headerName: 'ID' },
    { field: 'name', headerName: 'Name'},
    {
        field: 'price',
        headerName: 'Price',
        sortComparator: (v1, v2) => {
          return v1 - v2
        }
    },
    { field: 'stock', headerName: 'Stock'}
  }
]
```
昇順・降順を明示すると次のようになります。
``sortModel`` プロパティを用いてソート状態を管理し、昇順・降順の条件分岐ができます。 
``customSortComparator`` の返り値を色々いじってみてソートを試してみるのも良いと思います。
```react

const customSortComparator = (sortModel) => (v1, v2) => {
    if (sortModel.length === 0) {
        return 0 // ソートしない
    } else {
        if (sortModel[0].sort === 'asc') {
            return v1 - v2 // 昇順
        } else if (sortModel[0].sort === 'desc') {
            return v1 - v2 // 降順（v2 - v1 ではないことに注意）
        }
    }
}

export default function App() {
    const [sortModel, setSortModel] = React.useState([])
    const handleSortModel = (newSortModel) => {
        setSortModel(newSortModel)
    }

    const columns = [
        { field: 'id', headerName: 'ID' },
        { field: 'name', headerName: 'Name'},
        {
            field: 'price',
            headerName: 'Price',
            sortComparator: customSortComparator(sortModel)
        },
        { field: 'stock', headerName: 'Stock'}
    ]

    return (
        <Box>
            <DataGrid
                rows={rows}
                columns={columns}
                sortModel={sortModel} // sortModelプロパティを追加
                onSortModelChange={handleSortModel} // ハンドラを追加
            />
        </Box>
    )
}
```
:::note
``sortModel``  は要素数 ``0`` か ``1`` の配列で、要素にはソート対象列を表す ``field`` が入ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945817/2509431f-3f79-3d58-4bdc-63da6b30a80c.png)
``console.log(sortModel)`` で見るとこんな感じです。
ここでは割愛しますが、列によって異なるソートロジックを与えることも可能です。
:::

## ``sortComparator`` の応用
ここまでで話が終わってしまうと、わざわざ ``sortComparator`` を使う意味がありません。
**昇順・降順のソートは Data Grid に標準搭載されている**からです。
ようやくスタートラインに立ちました！ということで、ここからはカスタマイズの小技を紹介します。

### 特定の行を固定する
例えば、最上段の行、すなわち ``id === 1`` の行を固定してみます。
``id === 1`` の行が来たときに、返り値に ``0`` を与えるよう条件分岐します。
``id`` を用いるため、引数に ``row1, row2`` を追加します。

```react
const customSortComparator = (sortModel) => (v1, v2, row1, row2) => {
    if (sortModel.length === 0) {
        return 0 // ソートしない
    } else {
        if (sortModel[0].sort === 'asc') {
            if (row1.id === 1 || row2.id === 1) {
                return 0 // 先頭行を固定
            } else {
                return v1 - v2 // 昇順
            }
        } else if (sortModel[0].sort === 'desc') {
            if (row1.id === 1 || row2.id === 1) {
                return 0 // 先頭行を固定
            } else {
                return v1 - v2 // 降順
            }
        }
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945817/a36952fa-bca7-cd3b-7d4e-00d0cba423e2.png)

### 特定の行を下方に追いやる
例えば、データが歯抜けになっていて ``undefined`` で与えられる場合、式 ``v1-v2`` を評価できません。
```react
{ id: 3, name: 'Peach', stock: 10 } // price が定義されていない場合
```
ソートしたときに邪魔なので、 ``undefined`` の行を強制的に下方に送るようにします。

```react
const customSortComparator = (sortModel) => (v1, v2) => {
    if (sortModel.length === 0) {
        return 0 // ソートしない
    } else {
        if (sortModel[0].sort === 'asc') {
            if (v1 === undefined) {
                return 1 // v1 が未定義のとき、v1 を下方へ
            } else if (v2 === undefined) {
                return -1 //  v2 が未定義のとき、v2 を下方へ
            } else {
                return v1 - v2 // 昇順
            }
        } else if (sortModel[0].sort === 'desc') {
            if (v1 === undefined) {
                return -1 // 1 ではないことに注意
            } else if (v2 === undefined) {
                return 1 // -1 ではないことに注意
            } else {
                return v1 - v2 // 降順
            }
        }
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945817/7bfea539-1b88-1a4c-7eed-061f74add816.png)

## おわりに
Data Grid は MUI の中でも主力のコンポーネントなので、機能がかなり充実しています。ただし、痒いところに手が届くような機能は Pro や Premium といった有料プラン限定です。
紹介しておいてこう言うのも憚られますが、例えば [Row pinning](https://mui.com/x/react-data-grid/row-pinning/) があればソートのカスタマイズは必要ないかもしれません。

「SmartCloud（スマートクラウド）」は、NTTコムウェア株式会社の登録商標です。
その他、記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。