普通文組 2.5

「用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 版本:

  1. npm install --save typescript @types/node @types/react @types/react-dom @types/jest
  2. package.json 中的scripts/build內容改為tsc
  3. npx tsc --init
  4. 根據實際情況調整 tsconfig.json 中rootDirrootDir的設定(官方文件預設為srcbuild
  5. 設定jsxpreserve(參考討論串:Error when opening TSX file: Cannot use JSX unless ‘—jsx’ flag is provided
  6. .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 類型 peek event type

  • 需注意:在透過e.target.value取使用者輸入的值(value)的時候,取到的value會是string型態資料

    • MDN <input> element value: It can be altered or retrieved at any time using JavaScript to access the respective HTMLInputElement 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);
    };
  • 在將 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}
        />
        {/* 略 */}
      )
    }

    參考討論串:「How to define type for a function callback (as any function type, not universal any) used in a method parameter

  • 使用 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.VFCReact.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.Elementnull而不能是undefined
  • It provides typechecking and autocomplete for static properties like displayName, propTypes, and defaultProps. Note that there are some known issues using defaultProps with React.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 write import 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!

export

參考文件