React hooks 在 2019 年二月被引入,以改善代碼可讀性。本文將探討如何將其和 TypeScript 協同使用。前端
在 hooks 以前,有兩種風格的 React 組件:react
一種常見用法是,由前者構建複雜的容器(Container)組件,然後者負責簡單些的展現型(Presentational)組件。git
容器組件負責狀態(state)管理,以及在本文中被稱爲「反作用(side effects)」的遠端請求。狀態將經由 props 傳播到子組件。github
但隨着代碼的增加,函數式組件也大有取代類組件成爲容器的意思。typescript
將函數式組件升級爲狀態龐雜的容器卻是談不上痛苦,只是費時費力。此外,嚴格區分所謂容器和展現組件也不那麼被看重了。json
Hooks 能夠很好地兼顧, 能讓代碼既通用,又擁有幾乎全部的優勢。這裏有個例子,用來演示如何向一個處理報價簽署的組件中增添一個本地狀態:數組
// 在一個本地狀態中放置簽名,並在簽署狀態改變時切換籤名
function QuotationSignature({quotation}) {
const [signed, setSigned] = useState(quotation.signed);
useEffect(() => {
fetchPost(`quotation/${quotation.number}/sign`)
}, [signed]); // 簽署狀態改變時,反作用將被觸發
return <>
<input type="checkbox" checked={signed}
onChange={() => {setSigned(!signed)}}/>
Signature
</>
}
複製代碼
還有個利好不得不說 -- 雖然相比於 TypeScript 在 Angular 中的絲滑編碼,到了 React 中總被詬病臃腫難用;但 用 TypeScript 搭配 React hooks 卻變爲了一種愉悅的體驗。安全
TypeScript 由微軟設計並沿着 Angular 的路徑一路進發,而彼時 React 開發出的 Flow 已然式微。在 React 類組件中編寫原生 TypeScript 着實痛苦,由於 React 開發者不得不一樣時對 props
和 state
定義類型,即使兩者的許多屬性是相同的。服務器
按原來的方式來講,先得有一個 Quotation
類型,用來管理某些 CRUD 組件的 state 和 props。並在其相關的 state 中,建立一個 Quotation
類型的屬性,以及指示已簽署或未簽署的狀態。框架
interface QuotationLine {
price: number
quantity: number
}
interface Quotation{
id: number
title: string;
lines: QuotationLine[]
price: number
}
interface QuotationState{
readonly quotation: Quotation;
signed: boolean
}
interface QuotationProps{
quotation: Quotation;
}
class QuotationPage extends Component<QuotationProps, QuotationState> {
// ...
}
複製代碼
可是設想一下,在新建某個報價時咱們面臨的狀況,也就是 QuotationPage 還沒有向服務器成功請求到一個 id 時:以前定義的 QuotationProps 將沒法獲知這個關鍵的數字值 -- 不完整的數據也沒法被 Quotation 類型 精確 匹配。咱們可能不得不在 QuotationProps 接口中聲明更多的代碼:
interface QuotationProps{
// 除去 id 以外 Quotation 中的全部屬性:
title: string;
lines: QuotationLine[]
price: number
}
複製代碼
咱們拷貝了除去 id 以外的全部屬性搞出一個新類型。這...讓我回憶起在 Java 中,被不得不編寫的一大堆 DTO (譯註:Data Transfer Object,數據傳輸對象 -- 一種不包含業務邏輯的簡單容器,其行爲限於內部一致性檢查和基本驗證等) 所支配的恐懼。
爲了克服這種痛苦,咱們得在 TypeScript 的知識上補補課了。
經過使用 hooks,咱們就能夠摒棄以前的 QuotationState -- 能夠將其拆分爲不一樣的兩部分:
// ...
interface QuotationProps{
quotation: Quotation;
}
function QuotationPage({quotation}:QuotationProps) {
// 譯註:由兩個 useXXX 函數分攤了以前接口中的兩個屬性
const [quotation, setQuotation] = useState(quotation);
const [signed, setSigned] = useState(false);
// ...
}
複製代碼
經過拆分狀態,就省去了明確建立新的接口。本地狀態類型每每能推導出默認的狀態值。
由於 hooks 組件就是函數,故能夠編寫返回 React.FC<Props>
類型(譯註:FC 即 function components)的相同組件函數。這樣的函數顯式聲明瞭其函數式組件的返回類型,並明確了 props 類型。
const QuotationPage: FC<QuotationProps> = ({quotation}) => {
const [quotation, setQuotation] = useState(quotation);
const [signed, setSigned] = useState(false);
// ...
}
複製代碼
顯然,在 React hooks 中使用 TypeScript 比在類組件中容易。而且由於強類型對於代碼安全是個有力的保障,若是你的新項目中用了 hooks 就應該考慮採用 TypeScript 了。
在以前的 React hooks TypeScript 例子中,對於 QuotationProps 接口中的屬性如何使用、使用哪些,還是不甚了了、很有不便。
TypeScript 其實提供了很多「工具方法」,以便在 React 中描述接口時有效「降噪」。
Partial<T>
: T 類型全部鍵的任意子集Omit<T, 'x'>
: 除 x
以外的 T 類型全部鍵Pick<T, 'x', 'y', 'z'>
: 從 T 類型中明確拾取 x, y, z
鍵在咱們的用例中,能夠用 Omit<Quotation, 'id'>
的形式來將 id 排除在 Quotation 類型以外。結合 type
關鍵字反手就能甩出一個新類型。
Partial<T>
和 Omit<T>
並不存在於 Java 等大部分強類型語言中,但常在前端開發中以各類方式大展身手。它們簡化了類型定義的負擔。
type QuotationProps = Omit<Quotation, id>;
function QuotationPage({quotation}: QuotationProps){
const [quotation, setQuotation] = useState(quotation);
const [signed, setSigned] = useState(false);
// ...
}
複製代碼
固然,或許也能夠用 extends 更清晰地區分出持久化類型等:
interface Quote{
title: string;
lines: QuotationLine[]
price: number
}
interface PersistedQuote extends Quote{
id: number;
}
複製代碼
這樣在處理相關屬性時,也簡化了常見的 if
或 undefined
問題。
慎用 Partial<T>
,它基本不會帶來任何保障。
Pick<T, ‘x’|’y’>
是另外一種不用聲明新接口就能隨時定義新類型的方式。若是一個組件只須要簡單編輯報價標題的話:
type QuoteEditFormProps = Pick<Quotation, 'id'|'title'>
複製代碼
或直接在行內聲明:
function QuotationNameEditor({id, title}: Pick<Quotation, 'id'|'title'>){ ...}
複製代碼
別懷疑,我但是領域驅動設計(DDD - Domain Driven Design)的鐵桿擁躉。我並非懶得爲了聲明個新接口而懶得多寫兩行 -- 須要精確描述領域內命名時,我會使用接口;而出於保證本地代碼正確性、降噪的目的,我就使用這些 TS 工具語法。
React 團隊始終將 React 視爲一個函數式框架。過去他們使用類組件以處理自身狀態,如今有了 hooks 這種容許一個函數跟蹤組件狀態的技術。
interface Place{
city: string,
country: string
}
const initialState: Place = {
city: 'Rosebud',
country: 'USA'
};
function reducer(state: Place, action): Partial<Place> {
switch (action.type) {
case 'city':
return { city: action.payload };
case 'country':
return { country: action.payload };
}
}
function PlaceForm() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<form>
<input type="text" name="city"
onChange={(event) => {
dispatch({ type: 'city', payload: event.target.value})
}}
value={state.city} />
<input type="text" name="country"
onChange={(event) => {
dispatch({type: 'country', payload: event.target.value })
}}
value={state.country} />
</form>
);
}
複製代碼
這就是一個安全使用 Partial
的良好用例。
儘管 reducer 函數會被屢次執行,但相關的 useReducer
hook 將只被建立一次。
經過 天然而然地 將 reducer 函數定義在組件以外,代碼能夠被分割成多個獨立的函數,而不是都集中在一個類中並共同圍繞着其內部狀態。
這對可測試性大有裨益 -- 某些函數只處理 JSX,其餘一些只處理業務邏輯,等等。
你(幾乎)再也不須要高階組件(HOC - Higher Order Components)了。渲染屬性(render props)模式更易於編寫函數式組件。
這樣一來,閱讀代碼變得更容易了。代碼再也不是連綿混雜的 類/函數/模式,而僅僅是函數的集合。然而,由於這些函數並未附加到一個對象中,對它們命名可能有點難。
JavaScript 的樂趣在於你能以任何方式擺弄你的代碼。加上 TypeScript 後,你仍能夠用 keyof
訪問對象的全部鍵,也能使用類型聯合建立出晦澀難搞的某些東西 -- 怕了怕了。
要確保你的 tsconfig.json
設置了 "strict":true
選項。在項目動工前就檢查它,不然你將不得不重構不少東西!
對於以何種程度類型化代碼是有爭議的。你能夠手動定義全部東西,也可讓編譯器推斷出類型。這取決於 linter 工具的配置和團隊約定。
同時,你仍會遇到運行時錯誤!TypeScript 比 Java 簡單,而且迴避了泛型的協變/逆變問題。
在下例中,有一個 Animal 列表,以及一個相同的 Cat 列表。
interface Animal {}
interface Cat extends Animal {
meow: () => string;
}
const duck = { age: 7 };
const felix = {
age: 12,
meow: () => "Meow"
};
const listOfAnimals: Animal[] = [duck];
const listOfCats: Cat[] = [felix];
function MyApp() {
const [cats , setCats] = useState<Cat[]>(listOfCats);
// 問題1:再次被使用的 listOfCats 聲明爲了一個 Animal[]
const [animals , setAnimals] = useState<Animal[]>(listOfCats)
const [animal , setAnimal] = useState(duck)
return <div onClick={()=>{
animals.unshift(animal) // 問題2:指鴨爲貓
setAnimals([...animals]) // 形成 dirty forceUpdate
}}>
The first cat says {cats[0].meow()} // 不言而喻
</div>;
}
複製代碼
糟糕的是,因爲分別用 Cat[]
和 Animal[]
兩種泛型聲明瞭 listOfCats,然後把 listOfAnimals 中的 duck 錯誤地壓入了第二次聲明爲 Animal[]
的 listOfCats 數組 -- 僅從 TS 靜態語法上看是一個 Animal 進入了一個 Animal[]
,但這就讓隨後對第一次聲明爲 Cat[]
的 listOfCats 元素調用發生了運行時錯誤。
TypeScript 只有一種泛型的簡單 雙變(bivariant) 實現,以供 JS 開發者採用。若是對變量命名得當,就能很大程度上避免指鴨爲貓。
同時,存在向 TS 中增長 in
和 out
約束的提案( https://github.com/microsoft/TypeScript/issues/10717
),以支持協變和逆變。
查看更多前端好文
請搜索 fewelife 關注公衆號
轉載請註明出處