React + TypeScript 默認 Props 的處理

  • 2019 年 10 月 3 日
  • 筆記

React 中的默認 Props

通過組件的 defaultProps 屬性可為其 Props 指定默認值。

以下示例來自 React 官方文檔 – Default Prop Values

class Greeting extends React.Component {    render() {      return (        <h1>Hello, {this.props.name}</h1>      );    }  }    // Specifies the default values for props:  Greeting.defaultProps = {    name: 'Stranger'  };    // Renders "Hello, Stranger":  ReactDOM.render(    <Greeting />,    document.getElementById('example')  );

如果編譯過程使用了 Babel 的 transform-class-properties 插件,還可以這麼寫:

class Greeting extends React.Component {    static defaultProps = {      name: 'stranger'    }      render() {      return (        <div>Hello, {this.props.name}</div>      )    }  }

加入 TypeScript

加入 TypeScript 後

interface Props {    name?: string;  }    class Greeting extends React.Component<Props, {}> {    static defaultProps = {      name: "stranger",    };      render() {      return <div>Hello, {this.props.name}</div>;    }  }

此時不支持直接通過類訪問 defaultProps 來賦值以設置默認屬性,因為 React.Component 類型上並沒有該屬性。

// ?Property 'defualtProps' does not exist on type 'typeof Greeting'.ts(2339)  Greeting.defualtProps = {    name: "stranger",  };

默認屬性的類型

上面雖然實現了通過 defaultProps 來指定屬性的默認值,但 defaultProps 的類型是不受約束的,和 Props 沒有關聯上。以至於我們可以在 defaultProps 裏面放任何值,顯然這是不科學的。

class Greeting extends React.Component<Props, {}> {    static defaultProps = {      name: "stranger",      // 並不會報錯  +    foo: 1,  +    bar: {},    };   // ...  }

同時對於同一字段,我們不得不書寫兩次代碼。一次是定義組件的 Props,另一次是在 defaultProps 里。如果屬性有增刪或名稱有變更,兩個地方都需要改。

為了後面演示方便,現在給組件新增一個必填屬性 age:number

interface Props {    age: number;    name?: string;  }    class Greeting extends React.Component<Props, {}> {    static defaultProps = {      name: "stranger",    };      render() {      const { name, age } = this.props;      return (        <div>          Hello, {name}, my age is {age}        </div>      );    }  }

通過可選屬性抽取出來,利用 typeof 獲取其類型和必傳屬性結合來形成組件的 Props 可解決上面提到的兩個問題。

所以優化後的代碼成了:

const defaultProps = {    name: "stranger",  };    type Props = {    age: number;  } & Partial<typeof defaultProps>;    class Greeting extends React.Component<Props, {}> {    static defaultProps = defaultProps;      render() {      const { name, age } = this.props;      return (        <div>          Hello, {name}, my age is {age}        </div>      );    }  }

注意我們的 Props 是通過和 typeof defaultProps 組合而形成的,可選屬性中的 name 字段在整個代碼中只書寫了一次。

當我們更新了 defaultProps 時整個組件的 Props 也同步更新,所以 defaultProps 中的字段一定是組件所需要的字段。

默認值的判空檢查優化

講道理,如果屬性提供了默認值,在使用時,可不再需要判空,因為其一定是有值的。但 TypeScript 在編譯時並不知道,因為有默認值的屬性是被定義成可選的 ?

比如我們嘗試訪問 name 屬性的長度,

class Greeting extends React.Component<Props, {}> {    static defaultProps = defaultProps;      render() {      const { name } = this.props;      return (        <div>          {/* ?Object is possibly 'undefined'.ts(2532) */}          name length is {name.length}        </div>      );    }  }

因為此時我們的 Props 實際上是:

type Props = {    age: number;  } & Partial<typeof defaultProps>;  // 相當於:  type Props = {    age: number;    name?: string;  };

修正方法有多個,最簡單的是使用非空判定符/Non-null assertion operator

非空判定符

- name length is {name.length}  + name length is {name!.length}

這意味着每一處使用的地方都需要做類似的操作,當程序複雜起來時不太可控。但多數情況下應付日常使用,這樣已經夠了。

類型轉換

因為組件內部有默認值的保證,所以字段不可能為空,因此,可對組件內部使用非空的屬性類型來定義組件,而對外仍暴露原來的版本。

const Greeting = class extends React.Component<  -  Props,  +  Props & typeof defaultProps,    {}  > {    static defaultProps = defaultProps;      render() {      const { name } = this.props;      return (        <div>  -        name length is {name!.length}  +        name length is {name.length}        </div>      );    }  -};  +} as React.ComponentClass<Props>;

通過 as React.ComponentClass<Props> 的類型轉換,對外使用 Greeting 時屬性中 name 還是可選的,但組件內部實際使用的是 Props & typeof defaultProps,而不是 Partial<T> 版本的,所以規避了字段可能為空的報錯。

通過高階組件的方式封裝默認屬性的處理

通過定義一個高階組件比如 withDefaultProps 將需要默認屬性的組件包裹,將默認值的處理放到高階組件中,同樣可解決上述問題。

function withDefaultProps<P extends object, DP extends Partial<P>>(    dp: DP,    component: React.ComponentType<P>,  ) {    component.defaultProps = dp;    type RequiredProps = Omit<P, keyof DP>;    return (component as React.ComponentType<any>) as React.ComponentType<      RequiredProps & DP    >;  }

然後我們的組件則可以這樣來寫:

const defaultProps = {    name: "stranger",  };    interface Props {    name: string;    age: number;  }    const _Greeting = class extends React.Component<Props, {}> {    public render() {      const { name } = this.props;      return <div>name length is {name.length}</div>;    }  };    export const Greeting = withDefaultProps(defaultProps, _Greeting);

這種方式就比較通用一些,將 withDefaultProps 抽取成一個公共組件,後續其他組件都可使用。但此種情況下就沒有很好地利用已經定義好的默認值 defaultProps 中的字段,書寫 Props 時還需要重複寫一遍字段名。

相關資源