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
時還需要重復寫一遍字段名。