# React(MRWR)第 16 節:Getting Clever with Data Sorting
> Udemy課程:[Modern React with Redux [2023 Update]](https://www.udemy.com/course/react-redux/)
`20240203Sat.~20240209Fri.`
:::danger
16-260 Sorting in JavaScript
(數字 > return a-b 或 b-a)
16-261 Sorting Strings
(字串 > 用到localeCompare()幫忙排序)
16-263 Object Sort Implementation
(物件 > 先轉換、用typeof判斷,再排序)
16-268 React Fragments
(可加上key prop,且直接回傳children,不會衍生其他DOM)
:::
## 16-259. Adding Sorting to the Table
這章節主要處理資料排序問題。

在做sorting時,要注意我們是會整個row一起移動!
分成上升排序或下降排序:
* Ascending(上升):

* Decending(下降):

****
## 16-260. Reminder on Sorting in JavaScript
> 參考資料:
> 1. [JS 將陣列 Array 重新排列的 sort()](https://ithelp.ithome.com.tw/articles/10225733)
> 2. [ECMAS-Array.prototype.sort ( comparefn )](https://tc39.es/ecma262/#sec-array.prototype.sort)
> 3. [Day 28 咩色用得好 - Array.prototype.sort (part - 1)](https://ithelp.ithome.com.tw/articles/10308185)
> 4. [Day 29 咩色用得好 - Array.prototype.sort (part - 2)](https://ithelp.ithome.com.tw/articles/10308587)
在JS中做sorting,第一個想到的就是array method中的`sort()`。基於上學期剛修完演算法,好奇查了下`sort()`背後的演算法是什麼,找到了一篇在[stackoverflow](https://stackoverflow.com/questions/57763205/what-is-array-prototype-sort-time-complexity)的解釋。

他提到在不同瀏覽器所實現的演算法不同,例如:
* Firefox中是使用"merge sort"
* Chrome中則是使用一種混合merge sort+insertion sort的演算法,叫做"[Timsort](https://v8.dev/blog/array-sort#timsort)"(此算法時間複雜度遠小於排序演算法最佳複雜度O(nlogn))
回到`sort()`本身,其實`sort()`並不是單純去做sorting而已,他會先把我們放進去的array每一個元素轉成string形式,利用各個string的Unicode 編碼做排序的,所以就會碰到例如10排在3, 4, 5之前的怪異現象。

所以說當我們想在JS中使用`sort()`的話,就必須去新增一個"comparator(比較器)"函式(一般都用箭頭函式,這邊的comparator只是給它的一個代稱)。
這裡用簡單的實際案例看看。
首先,假如我們直接對data陣列做`sort()`,會得到[10, 3, 4, 5]的排序,因為在JS的`sort()`,他會先將這四個數字轉成字串,再用Unicode 編碼做排序,但是這不符合我們的期望。
```javascript!
const data = [5, 10, 4, 3]
data.sort(); //[10, 3, 4, 5]
```
於是,我們加上一個匿名函數,也就是先前提到的comparator函式,
```javascript!
const data = [5, 10, 4, 3]
data.sort((a, b) => {
return a - b;
}); //[3, 4, 5, 10]
```
可以用chrome的dev tool跑跑看:
* 第一種方法:

* 第二種方法:

但...這是為什麼呢?為什麼加上所謂的comparator函式,就可以成功排序了呢?我們先把a, b給印出來,看看這兩個參數到底代表著什麼:

先不急著研究,每次看到array method,就會想到阿傑的文章!發現在sort的部份,阿傑用了兩篇來解釋sort:
1. [Day 28 咩色用得好 - Array.prototype.sort (part - 1)](https://ithelp.ithome.com.tw/articles/10308185)
2. [Day 29 咩色用得好 - Array.prototype.sort (part - 2)](https://ithelp.ithome.com.tw/articles/10308587)
原來`sort()`可以帶入一個compareFn參數(這個參數代入與否為optional)。因此我們在直接使用`sort();`而沒有給予任何參數時,`sort()`會用預設的排序方法做排序,也就是根據Unicode 編碼做排序。
而compareFn參數,顧名思義就是一個函式(也就是先前提到的comparator function),這函式可以接收兩個參數a, b,分別代表要用來比較的元素,這個compareFn參數會根據回傳值來決定a, b的排列順序:
(此表格式用來定義排序方式,例如說要遞增?遞減?或是若不傳入compareFn參數,則利用unicode編碼由小到大排序。至於排序的實作方式如同前面提到的,在不同瀏覽器下,會有不同的實作演算法,不太理解這裡的意思的話,可以看看阿傑[Day29](https://ithelp.ithome.com.tw/articles/10308587)最後的程式碼說明,會清楚很多!)
| compareFn(a, b)回傳值 | a, b的排序 |
| --------------------- | --------------------- |
| > 0 | a排在b後面 e.g. [b,a] |
| < 0 | a排在b前面 e.g. [a,b] |
| === 0 | 照a, b原先的順序排序 |
這個時候再回來看下圖,就能更輕易理解為什麼要return a-b了,因為return a-b若回傳值大於0,那麼a就要排在後面,因此這裡就可以形成一個遞增的排序。

另外,可以從阿傑的[Day28](https://ithelp.ithome.com.tw/articles/10308185)文章最後看到,關於comparator function的注意事項,他必須符合以下幾點
(其中的reflexive反身性,symmetric對稱性, trensitive遞移性,剛好這陣子在念離散一直被這些概念瘋狂轟炸,念到懷疑人生,不過...看到原來這些知識也可以運用在這裡好感動)

****
## 16-261. Sorting Strings
> 參考資料:[String.prototype.localeCompare()](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare)
前面說過,若不傳入參數到sort() method,則JavaScript會對陣列中的元素,先轉成String的形式,再依照Unicode編碼排序,故若我們現在要對字串做排序,使用sort()就會相當的方便好用。
但是,如果同時出現大小寫,可能就不會照我們預期的排序了,這裡示範簡單的例子:
```javascript!
const data = ["t", "A", "a", "B", "b"]
data.sort();
```
結果如下所示,他會先把大寫排出,再排小寫,但我們希望的是所有的"a"都排在最前面,接著是"b",無論大寫還是小寫:

這裡會使用到一個JS內建的函式`localeCompare()`,這個函式其實就是所謂的comparator function,他會回傳正數、負數或是0,回傳值如下:

可參考下方範例:
```javascript!
// "a" 在 "c" 之前,所以會回傳負數
"a".localeCompare("c"); // -2、-1 或是其他負數值
// 按字母順序,「check」的順序在「against」之後,所以回傳正數
"check".localeCompare("against"); // 2、1 或其他正數值
// "a" 和 "a" 相同,所以回傳 0
"a".localeCompare("a"); // 0
```
因此當他得到回傳值之後,我們就能依照先前的表格對照,的確回傳負數代表著a排在b前面,所以基本上我們不需要再做其他的動作即可做好排序。
| compareFn(a, b)回傳值 | a, b的排序 |
| --------------------- | --------------------- |
| > 0 | a排在b後面 e.g. [b,a] |
| < 0 | a排在b前面 e.g. [a,b] |
| === 0 | 照a, b原先的順序排序 |
此時,我們把剛才的範例,加上`localeCompare()`,看看結果如何:
```javascript!
const data = ["t", "A", "a", "B", "b"]
data.sort((a, b) => {
return a.localeCompare(b);
});
```
結果如下所示:

****
## 16-262. Sorting Objects

在物件中做排序,有非常多方法,以上圖為例,我們可以依照weight排序、可以依照cost排序,也可以照name的第一個字母排序,當然我們甚至可以拿 cost/weight 算出每一單位重量要花多少錢來排序,又或者依照name的單字長度做排序(雖然沒啥意義)。
因此,這裡要做的就是從物件抽取我們要的資料即可,轉變成單純數字或單純字串,如此一來我們就可以拿著這些數字或字串,依照前面幾節學到的方法來排序。
****
## 16-263. Object Sort Implementation

實作程式碼:
```javascript!
const data = [
{weight: 15, cost: 5, name: "Onion"},
{weight: 10, cost: 2, name: "Tomato"},
{weight: 4, cost: 8, name: "Carrot"}
];
function getSortValue(vegitable){
return vegitable.cost;
};
data.sort((a, b) => {
const valueA = getSortValue(a);
const valueB = getSortValue(b);
return valueA - valueB;
})
```
實作結果:

所以若要針對物件中的某個值去做排序,最主要的就是getSortValue() 函式,因為他可以幫我們把物件中的屬性值轉換出來,讓我們可以去做比較。
現在的問題是,我們要排序的東西是"string"還是"number"?
因為從16-260與16-261大概可以看出我們針對"string"或是"number"的作法,是不同作法的。
因此,這裡將使用到`typeof`,來幫助我們判斷是"string"還是"number",之後再進一步做處理。
首先,先來看看[typeof](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/typeof)如何使用以及他的回傳值,基本上這裡只會用到"string"或是"number",其他更詳細的內容,可以到MDN上查詢。

實作程式碼:
```javascript!
const data = [
{weight: 15, cost: 5, name: "Onion"},
{weight: 10, cost: 2, name: "Tomato"},
{weight: 4, cost: 8, name: "Carrot"}
];
function getSortValue(vegitable){
return vegitable.cost;
};
data.sort((a, b) => {
const valueA = getSortValue(a);
const valueB = getSortValue(b);
if(typeof valueA === "string"){
return valueA.localeCompare(valueB);
}else{
return valueA - valueB;
}
})
```
這裡改用name做測試,測試結果如下:

所以說,如果想要改變sorting的目標,我們只需要更改getSortValue()即可。
例如說,我想要依照cost/weight算出每一單位重量要花多少錢來排序,那麼只要更改getSortValue()成如下所示,即可做排序:
```javascript!
function getSortValue(vegitable){
return vegitable.cost / vegitable.weight;
};
```
****
## 16-264. Reversing Sort Order
在[16-260](https://hackmd.io/vGXi5O3nQhaFk6S6XHUJvw?both#16-260-Reminder-on-Sorting-in-JavaScript)提過,遞增或是遞減排序,可以依照傳入的compareFn 參數來決定。
前幾節都是遞增排序,所以若我們想要遞減排序,其實只要在return值上乘上一個負號就可以了。
但問題是,若每次要統一成遞增或是遞減,都要對兩邊return值乘上1,或是乘上-1,動作非常瑣碎,且還找到return值的位置。
因此這裡我們一樣在外面建立一個變數`sortOrder`,讓它專門幫助我們處理要乘1(遞增)或是乘-1(遞減)。
實作程式碼:
```javascript!
const data = [
{weight: 15, cost: 5, name: "Onion"},
{weight: 10, cost: 2, name: "Tomato"},
{weight: 4, cost: 8, name: "Carrot"}
];
function getSortValue(vegitable){
return vegitable.cost;
};
const sortOrder = "ascending"
data.sort((a, b) => {
const valueA = getSortValue(a);
const valueB = getSortValue(b);
const reverseOrder = sortOrder === "ascending" ? 1 : -1;
if(typeof valueA === "string"){
return valueA.localeCompare(valueB) * reverseOrder;
}else{
return (valueA - valueB) * reverseOrder;
}
})
```
遞增結果:

遞減結果:

****
## 16-267. Customizing Header Cells
接下來的這幾節,基本上就是講如何將前面所學的排序應用到React中,重點在於要讓整個table reusable!!!
這裡會用到config變數幫助我們處理,主要在於"header",若config中出現"header",則會用"header"的函式去覆蓋原先的樣式或DOM元素等等。


****
## 16-268. React Fragments
* 問題:

* 問題解決:
利用React內建的方法解決,利用「Fragment元件」,幫我們達成想要加上key prop,卻又不希望產生多餘的DOM元素。
「Fragment元件」的原理老師上課有稍微提到,我們建立一個Echo元件,而此元件只會return children,至於children是什麼?children prop即是Echo元件裡面所包覆的東西,以此處為例便是`{column.header()}`

不過,React其實有內建一個叫做"Fragment"的元件,可以幫助我們做到像是上方Echo元件在做的事情,使用方法如下圖所示,有了"Fragment元件",即便需要添加key prop,也不必再額外渲染不必要的DOM元素了。

****
## 16-269. The Big Reveal
這裡稍做複習:


現在,我們要將先前提到的`getSortValue()`在需要時,可以加進config陣列中的任一物件裡使用。
不過,我們稍微改個名字,改成叫做`sortValue()`。

接著,我們還要再做一個元件叫做"SortableTable",在"SortableTable"將會寫入大量的邏輯,專注在處理排序我們的資料,以及顯示我們的header(就是每一欄位的名稱,例如name、color、score)是否要顯示可以排序的按鈕,而當顯示可排序按鈕時,使用者可以點擊,並且在點擊之後進行資料的排序。

****
## 16-270. Adding SortableTable
(這裡只條列出一些碰到可能會出錯的問題)
* 在React中,基本上不會去修改傳進來的props,所以像我們要針對config中擁有sortValue屬性的物件,對傳進來的config prop新增header屬性時,不是直接去新增,而是用mapping function,這樣才不會動到原先的config prop。
* Event Handlers with Arrow Functions

****
## 16-272. Quick State Design
* **Bad Idea:**

* **Good Idea:**

流程圖:

* **程式碼實作:**
// SortableTable
```javascript!
import { useState } from "react";
import Table from "./Table";
function SortableTable(props) {
const [sortOrder, setSortOrder] = useState(null);
const [sortBy, setSortBy] = useState(null);
const { config } = props;
const handleClick = (label) => {
if (sortOrder === null) {
setSortOrder("asc");
setSortBy(label);
} else if (sortOrder === "asc") {
setSortOrder("desc");
setSortBy(label);
} else if (sortOrder) {
setSortOrder(null);
setSortBy(null);
}
}
const updatedConfig = config.map((column) => {
if (!column.sortValue) return column;
return {
...column,
header: () =>
<th onClick={() => handleClick(column.label)}>
{column.label} IS SORTED
</th>
}
})
return (
<div>
{sortOrder} - {sortBy}
<Table {...props} config={updatedConfig} />
</div>
)
}
export default SortableTable;
```
****
## 16-275. Determining Icon Set
