npm package: @rybit/eslint-config-moov-base
Description: ESLint, not Eslint.
Latest version: 1.0.4

# ESLint

ESLint, not Eslint.

# Packages

- eslint-config-base
- eslint-config-typescript
- eslint-config-react
- eslint-config-express

# Usage

```javascript
// .eslintrc.js
{ 
  // Plain JavaScript project
  extends: [
    'moov-base',
  ],

  // React project with TypeScript
  extends: [
    'moov-base',
    'moov-typescript',
    'moov-react',
  ],

  // Express project with JavaScript
  extends: [
    'moov-base',
    'moov-express',
  ],
}
```

```json
// package.json
{
  "devDependencies": {
    "eslint-config-moov-base": "git+http://git.jetsbike.com/jetsbike/eslint-config-base",
    "eslint-config-moov-react": "git+http://git.jetsbike.com/jetsbike/eslint-config-react",
    "eslint-config-moov-typescript": "git+http://git.jetsbike.com/jetsbike/eslint-config-typescript"
  },
}
```

# 描述

各個 eslint config 主要繼承自 airbnb 所使用的 eslint rule，再視專案情況停用了部份 rules。每個 eslint config 之間不互相依賴，需依專案類型來決定使用哪些

其中，'moov-base'有定義到了 import-order，會將 import/require 分為四個 group:

- React/Express 這類 framework
- 其它 library，像是 lodash 等等的
- 專案中以`@/`開頭(套用 path aliases)的 import
- 其它 import

```typescript
import React, { useState } from 'react';

import ProTable, { ProColumns, ProTableProps } from '@ant-design/pro-table';
import { Button } from 'antd';
import _, { isNull } from 'lodash';
import moment from 'moment';

import { IntlProviderLocal, DownloadButton } from '@/components';
import { getFilterSearch, setFilterSearch } from '@/utils/utils';

import TableForm from './components/TableForm';
```

每個 group 再依英文字母順序排列其內容

# 解決它

大多的 error 可以依以下幾種方法解決

- `eslint --fix` 自動修正，或者 JetBrains 系列 IDE 可以用 action: "Fix ESLint Problems"。能解決像是 import-order 或者 prefer-const 這類簡單的問題
- 依 lint message 上的提示解決
- 在網路上搜尋這條 rule 的名稱，看看大家在什麼情況下遇到此 error 以及如何解決的
- 與你的好同事討論該怎麼辨
- 在加上`eslint-disable-next-line`之前，請務必再三確認你以及團隊成員都認同這麼作是 ok 的

# 常見的 lint error 解法

### PropType is defined but prop is never used(react/no-unused-prop-types)

```typescript
interface Props {
  detailLoading?: boolean; // <-
}

const SomeComponent: React.FC<Props> = (props) => {}
```

刪掉 component 的這個 prop，並且記得找到它的 usage，將原本有 pass 這個 prop 的地方也去掉，再確認有沒有 unused code

### 艾尼 (any)

````typescript
interface Props {
  // Don't use `{}` as a type. `{}` actually means "any non-nullish value".
  fetch: (params: FilterParams) => {};
  // Unexpected any. Specify a different type.(@typescript-eslint/no-explicit-any)
  auth: any;
}
````

大多情況下看到`any`時，請先翻找使用它的地方以及它是如何被使用的來確認它真正的 type，有時會需要回推到 call 該 api 的後端專案，或是從 db schema 中得知。如果它沒有一個固定的 type 的話，也可以考慮使用泛型(Generics)解決

如果到最後判定它真的只能是`any`的話，還是可以試著將之改為`Record<string, unknow>`至少能分別它是 Array、Object 還是 string, number 等等的基本型別。TypeScipt 中常常會以`Record<string, unknow>`來替代`Object, {}`或者其實它是`Object`的`any`

### Unexpected use of '|'.(no-bitwise)

```typescript
const failedCount = result?.length | 0;
```

確定沒有把`length || 0`誤打成`length | 0`嗎?

雖然以這個 case 來說它剛好也能正常運作 (`undefined | 0 === 0`)，但大多情況下你不需要用 bitwise，若真要使用的話請加`eslint-disable`

JavaScript 中也存在著許多好用、簡明，但容易被誤用或可讀性不佳的寫法，airbnb 的 ESLint rule 中已經把大多這類情況加入他們的 ESLint rule 中了，依循他們的規範，能在大多情況下使我們的專案更加得好維護

### 'params' is already declared in the upper scope.(@typescript-eslint/no-shadow)

```typescript
const { data, params } = useRequest(...);

function fetchData(params: FilterParams) {
//                 ^^^^^^
  props.fetch(params);
}
```

這個菜市場名已經有人用，撞名了

若直接換成`params2`的話就太沒創意了，可以考慮將`useRequest`的`params`改為`const { params: requestParams }`的這種寫法

### React Hook `useEffect` has missing dependencies: 'fetchData'. (react-hooks/exhaustive-deps)

```typescript
function fetchData(params: FilterParams) { /* .... */ }

useEffect(() => {
  fetchData( ... );
}, []); // <- [¯\_(ツ)_/¯]
```

雖然以目前情況下不作任何改動的話程式一樣會如預期的執行，但這個規則還是值得去注意一下

一般在寫一個沒有 dependencies 的`useEffect(() => {}, [])`即代表著希望在 component 載入時(`componentDidMount`)作一些資料的初始化。

而這條 rule 是在提示你說應當注意這些 function 或 object 會在每次 component render 時重建，並提示你應當也將它加入 `useEffect`的 dependencies 中，讓`useEffect`的 callback function 一併隨之重建

這時若直接把`fetchData`放到`useEffect`的 dependencies 的話，又會有另一個 lint error 出現

```typescript
// ESLint: The 'fetchData' function makes the dependencies of useEffect Hook (at line 90) change on every render. To fix this, wrap the definition of 'fetchData' in its own useCallback() Hook.(react-hooks/exhaustive-deps)
function fetchData(params: FilterParams) { /* .... */ }
```

它會提示說應當要改成`useCallback`(感覺麻煩)。好吧如果把`fecthData`函式移至`useEffect`中呢?

```typescript
useEffect(() => {
  function fetchData(params: FilterParams) { /* .... */ }
  fetchData();
}, []);
```

lint error 消失了，但是問題就在這個`fetchData`除了這邊用到之外，filter 條件改變後使用者按 submit button 時也會使用

所以我們還是乖乖得照建議把它放`useCallback`吧

```typescript
const fetchData = useCallback(
  (params: FilterParams) => {
    // do something to fetch
    dispatch({ type: 'fetch', params })
  },
  // some other dependencies
  [dispatch],
);

useEffect(() => {
  fetchData( ... );
}, [fetchData]);
```

沒什麼困難的對吧，那如果這個 component 的 filter 有個 defaultFilter，並且要存至 state 中呢

```typescript
const defaultFilter = {
  date: moment(),
  regionLevel: 'country',
  regionNames: [CountryList[0].value],
};
const [filterParams, setFilterParams] = useState(defaultFilter);

const fetchData = useCallback(
  (params: FilterParams) => {
    dispatch({ type: 'fetch', params: searchFormValue })
  },
  [dispatch],
);

useEffect(() => {
  fetchData(defaultFilter);
}, [fetchData]); // <- missing dependencies: 'defaultFilter'
```

它又會再度提示你那個 defaultFilter 沒有被加入 dependencies 中，而且還有另一個問題是這個 defaultFilter 在**每次 render 時都會重建**，也就是說上面由`moment()`產生的 date filed 在每次 render 時都會重新計算一次，浪費了一點點的效能並可能造成預期外的效果

而解法還是一樣繼續拿 hook 來用，這次用`useMemo`把 defaultValue 包起來

```typescript
const defaultFilterParams = useMemo<OrderReportSearchValue>(
  () => ({
    date: moment(),
    regionLevel: 'country',
    regionNames: [CountryList[0].value],
  }),
  [CountryList], // <- 別忘了加 dependencies
);
const [filterParams, setFilterParams] = useState(defaultFilterParams);

useEffect(() => {
  fetchData(defaultFilterParams);
}, [fetchData, defaultFilterParams]); 
```

只要確保`CountryList`的值不會變動的話，便可以放心得給`useEffect`多加上這個有`CountryList` dependencies 的`defaultFilter`

另外，如果直接在`useEffect`中 dependent 到`filterParams`這個 state 的話，會導致`filterParams`每次被更新時都**立即**去`fetchData`，而不是按 submit button 時才作

```typescript
const [filterParams, setFilterParams] = useState();

useEffect(() => {
  fetchData(filterParams);
}, [fetchData, filterParams]);
```

當然，你也可以說這不是 bug，是 feature

### JSX props should not use functions(react/jsx-no-bind)

```typescript
function handleShowSizeChange(current: number, pageSize: number) {
  fetchData({ pageNo: current, pageSize });
}

<TableForm
  onShowSizeChange={handleShowSizeChange}
/>
```

其實這也不是什麼大問題，只是個效能的 issue，因為這會導致每次 render 時都重建一次上面的`handleShowSizeChange`函式物件，增加 GC 的負擔而已

解法一樣簡單，給它包`useCallback`並妥善控管它 dependencies

```typescript
const handleShowSizeChange = useCallback((current: number, pageSize: number) => {
  fetchData({
    pageNo: current,
    pageSize,
  });
}, [fetchData]);

<TableForm
  onShowSizeChange={handleShowSizeChange}
/>
```

### React Hook "useState" is called conditionally. (react-hooks/rules-of-hooks)

```typescript
const SomeComponent: React.FC<Props> = (props) => {
  if (props.loading) {
    return <Spin style={{ width: '100%', height: '100%' }} />;
  }

  const [step, setStep] = useState(0);
  // ...
}
```

會這麼作，代表還沒理解`useState`是怎麼運作的。把`useState`放到會 return 的 condition 前即可

```typescript
const SomeComponent: React.FC<Props> = (props) => {
  const [step, setStep] = useState(0);
  
  if (props.loading) {
    return <Spin style={{ width: '100%', height: '100%' }} />;
  }
  // ...
}
```

### Missing trailing comma.(@typescript-eslint/comma-dangle)

```typescript
const defaultFilter = {
  regionLevel: 'country',
  regionNames: [CountryList[0].value]
//                                   ^
};
```

這是為了因應 git commit 時，如果你要加一個 property 在這個 object 上時，會同時改到前一行沒加`,`的，讓人以為那個`regionNames`也是這次 commit 修改的

```typescript
const defaultFilter = {
  regionLevel: 'country',
- regionNames: [CountryList[0].value]
+ regionNames: [CountryList[0].value],
+ bikeType: ['24']
};
```

如果`regionNames`原本就有 trailing comma 的話就只會有`bikeType`一行的改動

```typescript
const defaultFilter = {
  regionLevel: 'country',
  regionNames: [CountryList[0].value],
+ bikeType: ['24'],
};
```

沒什麼大問題，prettier 一下它會自動幫你加好加滿，還會一併把整份 code 都一併 prettier 掉

### Missing return type on function.(@typescript-eslint/explicit-module-boundary-types)

```typescript
export default function InputPrice(props: Props) { /* ... */ }
//             ^^^^^^^^
```

雖然 return type 還是可以靠 TypeScript 的 Type Inference 自動推斷，但如果函式本身就有定 type 的話便可以讓看的人更快速知道它會回傳什麼，也能讓 TypeScript engine 運作的更有效率

如果用的是 JetBrains 的 IDE，可以把游標移至函式名稱上，按`⌥Enter`來 Show Context Actions，然後按 Specify type explicitly 來自動補全 return type

```typescript
export default function InputPrice(props: Props): JSX.Element { /* ... */ }
```

另外，定 model type 時，用 interface 來定也會比用 type 定要來的高效，雖然 type 確實能用得比較靈活w

```typescript
interface PropsWithInterface {
  defaultValue?: number;
  disabled?: boolean | undefined;
}

type PropsWithType = {
  defaultValue?: number;
  disabled?: boolean | undefined;
};
```