「用TS改寫幼兒難度的React App」相關筆記
總結
快速入門 React 後,將其 JS 部分改為 TypeScript,記錄下相關筆記以及轉換途中遇到的坑 另外因為已經學過 Vue,故以下筆記會包含一些與 Vue 類似或相異的比較
JS 版原始碼:https://github.com/tzynwang/react-practice/tree/speed-converter TS 版原始碼:https://github.com/tzynwang/react-practice/tree/speed-converter-ts
教材內容取自:從 Hooks 開始,讓你的網頁 React 起來
版本
react: 17.0.2
typescript: 4.4.4
React 部分
- React components are regular JavaScript functions, but their names must start with a capital letter or they won’t work! 作為元件的 function 其名稱一定要大寫開頭
JSX
-
基本上接收任何資料(strings, numbers, and other JavaScript expressions),需注意在進行條件判斷的時候只能使用 expressions
App = () => { const [userInput, setUserInput] = useState(0); const handleUserInput = (e) => { setUserInput(e.target.value); }; return ( <div className="container"> <div className="card-header">Network Speed Converter</div> <div className="card-body"> <UnitControl /> <UnitConverter userInput={userInput} handleUserInput={handleUserInput} /> </div> <CardFooter userInput={userInput} /> </div> ); };
const UnitConverter = ({ userInput, handleUserInput }) => { return ( {/* 略 */} <input type="number" className="input-number" min="0" value={userInput} autoFocus onChange={handleUserInput} /> {/* 略 */} ) }
handleUserInput
透過 props 傳給UnitConverter
元件,UnitConverter
在 onChange 事件發生時,即可透過handleUserInput
修改userInput
的值 -
放 CSS Object 的話就多包一層
{}
const TodoList = () => { return ( <ul style={{ backgroundColor: 'black', color: 'pink', }} > <li>Improve the videophone</li> <li>Prepare aeronautics lectures</li> <li>Work on the alcohol-fuelled engine</li> </ul> ); };
-
JSX looks like HTML, but under the hood it is transformed into plain JavaScript objects. You can’t return two objects from a function without wrapping them into an array. This explains why you also can’t return two JSX tags without wrapping them into another tag or a fragment. 每個 react 元件只能回傳單一內容,超過一個元素請用
<></>
包起來;不能回傳超過一個元素是因為這些元件最後會被轉為 JS Object,複數個元素在 return 時一定要打包成單一元素 -
JSX requires tags to be explicitly closed: self-closing tags like
<img>
must become<img />
, and wrapping tags like<li>oranges
must be written as<li>oranges</li>
. 所有的HTML 元素一定要 closed(舉例:<br>
要改寫為<br />
) -
JSX turns into JavaScript and attributes written in JSX become keys of JavaScript objects.
useState()
- Returns a stateful value, and a function to update it.
- 放到
useState(...)
裡的變數才會被 React 追蹤,概念類似資料要放在data()
中才會被 Vue 追蹤、修改後才會跟著驅動畫面更新
改裝為 TS
將 TS 加入專案中
透過以下手續將原本是 JS 的 React 專案改為 TS 版本:
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
- package.json 中的
scripts/build
內容改為tsc
npx tsc --init
- 根據實際情況調整 tsconfig.json 中
rootDir
與rootDir
的設定(官方文件預設為src
與build
) - 設定
jsx
為preserve
(參考討論串:Error when opening TSX file: Cannot use JSX unless ‘—jsx’ flag is provided) .js
副檔名改為.ts
或.tsx
(enable JSX with TypeScript,參考TS 官方文件)
完整說明參考 React 官方文件:Adding TypeScript to a Project
備註:如果是要從零開始一個 React+TS 專案的話,直接npx create-react-app <app-name> --template typescript
即可
將原始碼改為 TS
-
偷吃步:在 vs code 中如果不知道該 event 的類型,可以在
{}
中寫出 e 然後透過滑鼠 hover 來查看 event 類型 -
需注意:在透過
e.target.value
取使用者輸入的值(value
)的時候,取到的value
會是string
型態資料- MDN
<input>
element value: It can be altered or retrieved at any time using JavaScript to access the respectiveHTMLInputElement
object’s value property. - MDN
HTMLInputElement
value property: It is string, returns / sets the current value of the control. - 所以
handleUserInput
在改寫成 TS 版加上的 type 必須為string
:
const handleUserInput = (e: ChangeEvent<{ value: string }>) => { const rawInput = Number(e.target.value); if (isNaN(rawInput)) return; setUserInput(rawInput); };
- MDN
-
在將 function
handleUserInput
透過 props 傳給元件UnitConverter
的時候想不通「function 的類型究竟是什麼」,透過關鍵字搜尋後發現與其說是 function 的「type」是什麼,不如說是告訴 TS「這個 function 的 shape 是什麼形狀」// in UnitConverter.tsx interface propsType { userInput: number // handleUserInput的形狀是:接受e做為參數傳入,並「不回傳」任何值,所以 => void handleUserInput: (e: ChangeEvent<{ value: string }>) => void } const UnitConverter = ({ userInput, handleUserInput }: propsType) => { return ( {/* 略 */} <input type="number" className="input-number" min="0" value={userInput} autoFocus onChange={handleUserInput} /> {/* 略 */} ) }
-
使用 deconstruct 技巧處理 props 又必須給予 type 時,可使用以下寫法:
const CardFooter = (props: { userInput: number }) => { /* 略 */ };
或是這樣:
const CardFooter = ({ userInput }: { userInput: number }) => { /* 略 */ };
或是這樣:
interface userInputType { userInput: number; } const CardFooter = ({ userInput }: userInputType) => { /* 略 */ };
以上做的都是「設定 props 中 userInput 的類型為
number
」同一件事情
關於 React.FC
參考 React+TypeScript Cheatsheets,小總結:不建議使用React.FC
,可改用React.VFC
;React.FC
與一般 function 不同的部分如下列:
React.FunctionComponent
is explicit about the return type, while the normal function version is implicit (or else needs additional annotation).- 引用自 PJ:
React.FC
會明確定義 Function Component 回傳的型別一定要是JSX.Element
或null
而不能是undefined
。 - It provides typechecking and autocomplete for static properties like
displayName
,propTypes
, anddefaultProps
. Note that there are some known issues usingdefaultProps
withReact.FunctionComponent
. - It provides an implicit definition of children - however there are some issues with the implicit children type (e.g. DefinitelyTyped#33006), and it might be better to be explicit about components that consume children, anyway.
- 引用自 PJ:
React.FC
會自動接受children
作為可以傳入的 props,即使在這個 React 元件中並不會使用到children
。
補充
named export, default export
- If a module defines a default export, then you can import that default export by omitting the curly braces.
// foo.js export default function foo() { console.log('hello!'); }
import foo from 'foo'; foo(); // hello!
- A file can have no more than one default export, but it can have as many named exports as you like. export default 只能有一個,但 named exports 可以有無數個
- When you write a default import, you can put any name you want after
import
. For example, you could writeimport Banana from './button.js'
instead and it would still provide you with the same default export. - In contrast, with named imports, the name has to match on both sides. That’s why they are called named imports!