使用 TypeScript 開發 React 函數式組件

前言

在我們使用 React 開發項目時,使用最多的應該都是組件,組件又分為函數組件類組件,我們可以這麼定義:

定義函數組件:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

定義類組件:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

這篇文章我會和大傢介紹使用 TypeScript 定義函數式組件的 4 種方法,還有幾個使用過程中需要註意的問題。

如何使用 TypeScript 定義函數式組件

函數式組件通常接受一個 props 參數,返回一個 JSX 元素或者 null

當我們需要使用 TypeScript 去定義一個函數式組件時,我們有 4 種方式,4 種方式各有各的優缺點,看具體情況使用。

1. 使用 React.FC

由於 React 不是使用 TypeScript 開發的,使用的是社區開發的 @type/react 包提供的類型,裡面有一個通用類型 FC ,允許我們為函數組件添加類型。

type FCProps = { text: string };
// React.FunctionComponent 的簡寫
const FCComponent: React.FC<FCProps> = ({ text = "" }) => <div>{text}</div>;

這裡的 React.FC 是 React.FunctionComponent 的簡寫。

當組件包含子元素,TypeScript 會提示警告:

type FCProps = { text: string };
const FCComponent: React.FC<FCProps> = ({ text = "" }) => <div>{text}</div>;

function App() {
  return (
    <div className="App">
        <FCComponent text="Hello Chris1993.">
            <span>children</span>
        </FCComponent>
    </div>
  );
}

提示警告內容:

Type '{ children: string; text: string; }' is not assignable to type 'IntrinsicAttributes & FCProps'.
  Property 'children' does not exist on type 'IntrinsicAttributes & FCProps'.

現在不推薦使用這個瞭,具體討論可以看這兩個鏈接:

  • Remove React.FC from Typescript template #8177;
  • 《TypeScript + React: Why I don't use React.FC》。

2. 使用 JSX.Element

使用 JSX.Element 類型作為函數式組件的返回值類型,當組件的返回值不是 JSX.Element 類型時,TypeScript 就會提示錯誤。

type FCProps = { text: string };
const ElementComponent = ({ text }: FCProps): JSX.Element => <div>{text}</div>;
function App() {
  return (
    <div className="App">
        <ElementComponent text="Hello Chris1993."></ElementComponent>
    </div>
  );
}

3. 直接定義完整類型

由於 React 組件包含子元素時,會隱式傳遞一個 children 屬性,導致定義的參數類型出錯,因此我們可以直接定義一個完整的參數接口,包含瞭 children 屬性的類型:

type FCProps = { text: string; children?: any };
const FCComponent: React.FC<FCProps> = ({ text = "" }) => <div>{text}</div>;

function App() {
  return (
    <div className="App">
        <FCComponent text="Hello Chris1993.">
            <span>children</span>
        </FCComponent>
    </div>
  );
}

4. 使用 React.PropsWithChildren

第 3 種方法每次都要手動寫一個 children 屬性類型比較麻煩,這時候我們就可以使用 React.PropsWithChildren 類型,它本身封裝瞭 children 的類型聲明:

// react/index.d.ts
type PropsWithChildren<P> = P & { children?: ReactNode };

因此,使用 React.PropsWithChildren 類型定義函數式組件,就不用去處理 children 的類型瞭:

type IProps = React.PropsWithChildren<{ text: string }>;
const PropsComponent = ({ text }: IProps) => <div>{text}</div>;
function App() {
  return (
    <div className="App">
        <PropsComponent text="Hello Chris1993.">
            <span>children</span>
        </PropsComponent>
    </div>
  );
}

使用過程需要註意的點

1. 函數式組件返回值不能是佈爾值

當我們在函數式組件內使用條件語句時,如果返回的是非 JSX 元素或者非 null 的值,React 將會報錯:

const ConditionComponent = ({ useRender = false }) =>
  useRender ? <span>Render ConditionComponent</span> : false;// ❌

function App() {
  return (
    <div className="App">
        <ConditionComponent useRender></ConditionComponent>
        {/* 'ConditionComponent' cannot be used as a JSX component.
            Its return type 'false | Element' is not a valid JSX element.
            Type 'boolean' is not assignable to type 'ReactElement<any, any>'.
        */}
    </div>
  );
}

正確的處理方式,應該是讓函數式組件返回一個有效的 JSX 元素或者 null:

const ConditionComponent = ({ useRender = false }) =>
  useRender ? <span>Render ConditionComponent</span> : <span>error</span>;// ✅

// or

const ConditionComponent = ({ useRender = false }) =>
  useRender ? <span>Render ConditionComponent</span> : null;// ✅

當然你也不能這樣寫,當屬性 useRender 為 true 時,也會出錯:

const ConditionComponent = ({ useRender = false }) =>
  useRender && <span>Render ConditionComponent</span>;// ❌

2. 無法為組件使用 Array.fill() 填充

當我們的組件直接返回 Array.fill() 的結果時,TypeScript 會提示錯誤。

const ArrayComponent = () => Array(3).fill(<span>Chris1993</span>); // ❌

function App() {
  return (
    <div className="App">
      <ArrayComponent></ArrayComponent>
    </div>
  );
}

提示下面內容:

'ArrayComponent' cannot be used as a JSX component.
  Its return type 'any[]' is not a valid JSX element.
    Type 'any[]' is missing the following properties from type 'ReactElement<any, any>': type, props, key

為瞭解決這個問題,我們可以定義函數的返回值類型:

const ArrayComponent = () =>
  Array(3).fill(<span>Chris1993</span>) as any as JSX.Element; // ✅

3. 支持使用泛型來創建組件

在使用 TypeScript 開發 React 函數式組件的時候,也可以使用泛型進行約束,聲明一個泛型組件(Generic Components),這樣可以讓我們的組件更加靈活。

可以這樣使用:

interface GenericProps<T> {
  content: T;
}
const GenericComponent = <T extends unknown>(props: GenericProps<T>) => {
  const { content } = props;
  const component = <>{content}</>;
  return <div>{component}</div>;
};
function App() {
  return (
    <div className="App">
      { /* Success ✅ */}
      <GenericComponent<number> content={10} />
      { /* Error ❌ Type 'string' is not assignable to type 'number'. */}
      <GenericComponent<number> content={"10"} />
    </div>
  );
}

在 Generic Components 章節中介紹到更高級的使用方式:

interface Props<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

const List = <T extends unknown>(props: Props<T>) => {
  const { items, renderItem } = props;
  const [state, setState] = React.useState<T[]>([]); // You can use type T in List function scope.
  return (
    <div>
      {items.map(renderItem)}
      <button onClick={() => setState(items)}>Clone</button>
      {JSON.stringify(state, null, 2)}
    </div>
  );
};
function App() {
  return (
    <div className="App">
        <List<number>
          items={[1, 2]} // type of 'string' inferred
          renderItem={(item) => (
            <li key={item}>
              {/* Error: Property 'toPrecision' does not exist on type 'string'. */}
              {item.toPrecision(3)}
            </li>
          )}
        />
    </div>
  );
}

到此這篇關於使用 TypeScript 開發 React 函數式組件的文章就介紹到這瞭,更多相關TypeScript 開發 React內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: