使用 React Hooks 和 TypeScript

已發表: 2022-03-11

Hooks 於 2019 年 2 月被引入 React,以提高代碼的可讀性。 我們已經在之前的文章中討論了 React 鉤子,但這次我們正在研究鉤子如何與 TypeScript 一起工作。

在 hooks 之前,React 組件曾經有兩種風格:

  • 處理狀態的
  • 完全由它們的 props 定義的函數

這些的自然用途是構建具有類的複雜容器組件和具有純函數的簡單表示組件。

什麼是 React Hooks?

容器組件處理狀態管理和對服務器的請求,本文將在副作用中調用這些組件。 狀態將通過 props 傳播到容器子級。

什麼是 React Hooks?

但是隨著代碼的增長,功能組件往往會被轉換為容器組件。

將功能組件升級為更智能的組件並沒有那麼痛苦,但這是一項耗時且令人不快的任務。 此外,不再歡迎非常嚴格地區分演示者和容器。

Hooks 可以兩者兼得,因此生成的代碼更加統一,並且具有幾乎所有的好處。 這是一個將本地狀態添加到處理引用簽名的小組件的示例。

 // put signature in local state and toggle signature when signed changes function QuotationSignature({quotation}) { const [signed, setSigned] = useState(quotation.signed); useEffect(() => { fetchPost(`quotation/${quotation.number}/sign`) }, [signed]); // effect will be fired when signed changes return <> <input type="checkbox" checked={signed} onChange={() => {setSigned(!signed)}}/> Signature </> }

這有一個很大的好處——TypeScript編碼在 Angular 中很棒,但在 React 中顯得臃腫。 然而,使用 TypeScript 編寫 React 鉤子是一種愉快的體驗。

帶有舊 React 的 TypeScript

TypeScript 是由微軟設計的,並且在 React 開發Flow時遵循了 Angular 的路徑,但現在已經失去了吸引力。 用樸素的 TypeScript 編寫 React 類是相當痛苦的,因為即使許多鍵相同,React 開發人員也必須同時輸入propsstate

這是一個簡單的域對象。 我們製作了一個報價應用程序,具有Quotation類型,在一些帶有狀態和道具的 crud 組件中進行管理。 可以創建Quotation單,其關聯狀態可以更改為已簽名或未簽名。

 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 :例如,公司的第 678 條報價。 好吧,這意味著 QuotationProps 不知道那個重要的數字——它沒有完全包裝一個 Quotation 。 我們必須在 QuotationProps 接口中聲明更多代碼:

 interface QuotationProps{ // ... all the attributes of Quotation but id title:string; lines:QuotationLine[] price: number }

我們將除 id 之外的所有屬性複製到一個新類型中。 嗯。 這讓我想起了舊的 Java,它涉及編寫一堆 DTO。 為了克服這個問題,我們將增加我們的 TypeScript 知識以繞過痛苦。

帶有 Hooks 的 TypeScript 的好處

通過使用鉤子,我們將能夠擺脫之前的 QuotationState 接口。 為此,我們將 QuotationState 拆分為狀態的兩個不同部分。

 interface QuotationProps{ quotation:Quotation; } function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }

通過拆分狀態,我們不必創建新接口。 本地狀態類型通常由默認狀態值推斷。

帶有鉤子的組件都是函數。 因此,我們可以編寫返回 React 庫中定義的FC<P>類型的相同組件。 該函數顯式聲明其返回類型,並沿 props 類型設置。

 const QuotationPage : FC<QuotationProps> = ({quotation}) => { const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }

顯然,使用帶有 React 鉤子的 TypeScript 比使用 React 類更容易。 而且因為強類型對於代碼安全來說是一種有價值的安全措施,所以如果你的新項目使用鉤子,你應該考慮使用 TypeScript。 如果你想要一些 TypeScript,你絕對應該使用鉤子。

有很多原因可以避免使用 TypeScript,無論是否使用 React。 但是,如果您確實選擇使用它,那麼您也絕對應該使用鉤子。

適用於 Hooks 的 TypeScript 的具體功能

在前面的 React hooks TypeScript 示例中,我在 QuotationProps 中仍然有 number 屬性,但目前還不知道這個數字到底是什麼。

TypeScript 為我們提供了一長串實用程序類型,其中三個將通過減少許多接口描述的噪音來幫助我們使用 React。

  • Partial<T> : T 的任何子鍵
  • Omit<T, 'x'> : T 的所有鍵,除了鍵x
  • Pick<T, 'x', 'y', 'z'> : 正是來自Tx, y, z

適用於 Hooks 的 TypeScript 的具體功能

在我們的例子中,我們希望Omit<Quotation, 'id'>省略引用的 id。 我們可以使用type關鍵字動態創建一個新類型。

Partial<T>Omit<T>在大多數類型化語言(如 Java)中不存在,但對於前端開發中的 Forms 示例有很大幫助。 它簡化了打字的負擔。

 type QuotationProps= Omit<Quotation, id>; function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); // ... }

現在我們有一個沒有 id 的報價單。 所以,也許我們可以設計一個QuotationPersistedQuotation extends Quotation 。 此外,我們將輕鬆解決一些經常出現的ifundefined問題。 我們還應該調用變量引用嗎,儘管它不是完整的對象? 這超出了本文的範圍,但我們稍後會提到它。

但是,現在我們確信我們不會傳播我們認為有number的對象。 使用Partial<T>並不能帶來所有這些保證,因此請謹慎使用。

Pick<T, 'x'|'y'>是另一種動態聲明類型的方法,無需聲明新接口。 如果是組件,只需編輯引用標題:

 type QuoteEditFormProps= Pick<Quotation, 'id'|'title'>

要不就:

 function QuotationNameEditor({id, title}:Pick<Quotation, 'id'|'title'>){ ...}

不要評判我,我是領域驅動設計的忠實粉絲。 我並不懶惰到不想為新界面多寫兩行。 我使用接口來精確描述域名,這些實用函數用於本地代碼正確性,避免噪音。 讀者會知道Quotation是規範的接口。

React Hooks 的其他好處

React 團隊始終將 React 視為一個功能框架。 他們使用類以便組件可以處理自己的狀態,現在掛鉤作為一種允許函數跟踪組件狀態的技術。

 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是安全且不錯的選擇的案例。

雖然一個函數可以被多次執行,但關聯的useReducer鉤子只會被創建一次。

通過自然地從組件中提取reducer函數,代碼可以分成多個獨立的函數,而不是一個類內部的多個函數,都與類內部的狀態相關聯。

這顯然對可測試性更好——一些函數處理 JSX,其他處理行為,其他處理業務邏輯,等等。

你(幾乎)不再需要高階組件了。 使用函數編寫渲染道具模式更容易。

因此,閱讀代碼更容易。 您的代碼不是類/函數/模式流,而是函數流。 但是,由於您的函數未附加到對象,因此可能很難命名所有這些函數。

TypeScript 仍然是 JavaScript

JavaScript 很有趣,因為您可以從任何方向撕毀您的代碼。 使用 TypeScript,您仍然可以使用keyof來處理對象的鍵。 你可以使用類型聯合來創建一些不可讀和不可維護的東西——不,我不喜歡那些。 您可以使用類型別名來假裝字符串是 UUID。

但是你可以在零安全的情況下做到這一點。 確保您的tsconfig.json具有"strict":true選項。 在項目開始之前檢查它,否則你將不得不重構幾乎每一行!

關於您在代碼中輸入的級別存在爭議。 您可以鍵入所有內容或讓編譯器推斷類型。 這取決於 linter 配置和團隊選擇。

此外,您仍然可以犯運行時錯誤! TypeScript 比 Java 更簡單,並且避免了泛型的協變/逆變問題。

在此 Animal/Cat 示例中,我們有一個與 Cat 列表相同的 Animal 列表。 不幸的是,這是第一行的合同,而不是第二行。 然後,我們將一隻鴨子添加到 Animal 列表中,因此 Cat 的列表為 false。

 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); // Here the thing: listOfCats is declared as a Animal[] const [animals , setAnimals] = useState<Animal[]>(listOfCats) const [animal , setAnimal] = useState(duck) return <div onClick={()=>{ animals.unshift(animal) // we set as first cat a duck ! setAnimals([...animals]) // dirty forceUpdate } }> The first cat says {cats[0].meow()}</div>; }

TypeScript 只有一種用於泛型的雙變量方法,這種方法很簡單,有助於 JavaScript 開發人員採用。 如果您正確命名變量,您很少會在listOfCats中添加duck

此外,還有一個關於添加和輸出協變和逆變合同的建議。

結論

我最近回到了 Kotlin,它是一種很好的類型化語言,正確鍵入複雜的泛型是很複雜的。 由於鴨子類型和雙變量方法的簡單性,您沒有使用 TypeScript 的一般複雜性,但是您還有其他問題。 也許你在使用 TypeScript 和為 TypeScript 設計的 Angular 上沒有遇到很多問題,但是你在 React 類中遇到了這些問題。

TypeScript 可能是 2019 年的大贏家。它獲得了 React,但也憑藉 Node.js 及其使用非常簡單的聲明文件鍵入舊庫的能力征服了後端世界。 它埋葬了 Flow,儘管有些與 ReasonML 一路走來。

現在,我覺得 hooks+TypeScript 比 Angular 更令人愉快和高效。 六個月前我不會這麼想。