@rybit/eslint-config-moov-base
v1.0.4
Published
ESLint, not Eslint.
Downloads
102
Readme
ESLint
ESLint, not Eslint.
Packages
- eslint-config-base
- eslint-config-typescript
- eslint-config-react
- eslint-config-express
Usage
// .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',
],
}
// 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
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)
interface Props {
detailLoading?: boolean; // <-
}
const SomeComponent: React.FC<Props> = (props) => {}
刪掉 component 的這個 prop,並且記得找到它的 usage,將原本有 pass 這個 prop 的地方也去掉,再確認有沒有 unused code
艾尼 (any)
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)
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)
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)
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 出現
// 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
中呢?
useEffect(() => {
function fetchData(params: FilterParams) { /* .... */ }
fetchData();
}, []);
lint error 消失了,但是問題就在這個fetchData
除了這邊用到之外,filter 條件改變後使用者按 submit button 時也會使用
所以我們還是乖乖得照建議把它放useCallback
吧
const fetchData = useCallback(
(params: FilterParams) => {
// do something to fetch
dispatch({ type: 'fetch', params })
},
// some other dependencies
[dispatch],
);
useEffect(() => {
fetchData( ... );
}, [fetchData]);
沒什麼困難的對吧,那如果這個 component 的 filter 有個 defaultFilter,並且要存至 state 中呢
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 包起來
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 時才作
const [filterParams, setFilterParams] = useState();
useEffect(() => {
fetchData(filterParams);
}, [fetchData, filterParams]);
當然,你也可以說這不是 bug,是 feature
JSX props should not use functions(react/jsx-no-bind)
function handleShowSizeChange(current: number, pageSize: number) {
fetchData({ pageNo: current, pageSize });
}
<TableForm
onShowSizeChange={handleShowSizeChange}
/>
其實這也不是什麼大問題,只是個效能的 issue,因為這會導致每次 render 時都重建一次上面的handleShowSizeChange
函式物件,增加 GC 的負擔而已
解法一樣簡單,給它包useCallback
並妥善控管它 dependencies
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)
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 前即可
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)
const defaultFilter = {
regionLevel: 'country',
regionNames: [CountryList[0].value]
// ^
};
這是為了因應 git commit 時,如果你要加一個 property 在這個 object 上時,會同時改到前一行沒加,
的,讓人以為那個regionNames
也是這次 commit 修改的
const defaultFilter = {
regionLevel: 'country',
- regionNames: [CountryList[0].value]
+ regionNames: [CountryList[0].value],
+ bikeType: ['24']
};
如果regionNames
原本就有 trailing comma 的話就只會有bikeType
一行的改動
const defaultFilter = {
regionLevel: 'country',
regionNames: [CountryList[0].value],
+ bikeType: ['24'],
};
沒什麼大問題,prettier 一下它會自動幫你加好加滿,還會一併把整份 code 都一併 prettier 掉
Missing return type on function.(@typescript-eslint/explicit-module-boundary-types)
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
export default function InputPrice(props: Props): JSX.Element { /* ... */ }
另外,定 model type 時,用 interface 來定也會比用 type 定要來的高效,雖然 type 確實能用得比較靈活w
interface PropsWithInterface {
defaultValue?: number;
disabled?: boolean | undefined;
}
type PropsWithType = {
defaultValue?: number;
disabled?: boolean | undefined;
};