MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 中 defaultProps 的使用场景

2021-04-037.8k 阅读

一、React 基础回顾

在深入探讨 defaultProps 的使用场景之前,我们先来回顾一下 React 的一些基础知识。React 是一个用于构建用户界面的 JavaScript 库,它采用组件化的开发模式,将整个应用拆分成一个个独立的、可复用的组件。

每个 React 组件本质上是一个 JavaScript 函数或者类。函数式组件通过接收 props(属性)来渲染不同的 UI 内容,而类组件除了可以接收 props,还拥有自己的状态(state)。

(一)函数式组件

函数式组件是最简单的 React 组件形式,它是一个纯函数,接收 props 作为参数并返回一个 React 元素。例如:

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

在上述代码中,Welcome 组件接收一个名为 nameprops,并将其渲染在 <div> 标签内。

(二)类组件

类组件基于 ES6 的 class 语法,继承自 React.Component。类组件可以拥有 state 和生命周期方法。例如:

import React, { Component } from 'react';

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return <div>Count: {this.state.count}</div>;
  }
}

Counter 类组件中,通过 this.state 来管理组件内部的状态,props 则可以通过 this.props 来访问。

二、理解 defaultProps

defaultProps 是 React 提供的一种机制,用于为组件的 props 设置默认值。当组件在使用过程中没有传递某个 props 时,React 会使用 defaultProps 中设置的默认值。

(一)在函数式组件中使用 defaultProps

在函数式组件中,defaultProps 作为组件的一个属性来定义。例如,对于前面的 Welcome 组件,我们可以这样设置 defaultProps

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

Welcome.defaultProps = {
  name: 'Guest'
};

现在,如果在使用 Welcome 组件时没有传递 name 属性,它将默认显示 Hello, Guest。例如:

function App() {
  return <Welcome />;
}

这里 <Welcome /> 没有传递 name 属性,但由于设置了 defaultProps,依然能正常渲染出有意义的内容。

(二)在类组件中使用 defaultProps

在类组件中,defaultProps 是类的一个静态属性。例如,对于 Counter 类组件,我们可以添加一个 stepprops 并设置其默认值:

import React, { Component } from 'react';

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return <div>Count: {this.state.count} Step: {this.props.step}</div>;
  }
}

Counter.defaultProps = {
  step: 1
};

这样,当使用 Counter 组件时如果没有传递 step 属性,它将默认使用 1。例如:

function App() {
  return <Counter />;
}

上述代码中 <Counter /> 未传递 step,但依然能正常渲染出 Step: 1

三、defaultProps 的使用场景

(一)提供初始默认值

在很多情况下,我们希望组件在初始化时就有一些合理的默认值。比如一个展示用户信息的组件,可能有一个 avatar(头像)的 props。如果用户没有提供具体的头像地址,我们可以设置一个默认的通用头像。

function UserProfile(props) {
  return (
    <div>
      <img src={props.avatar} alt="User Avatar" />
      <p>{props.name}</p>
    </div>
  );
}

UserProfile.defaultProps = {
  avatar: 'default_avatar_url'
};

这样,无论用户是否传递 avatar 属性,UserProfile 组件都能正常渲染,并且在没有特定头像时展示默认头像。

(二)简化组件使用

当组件有多个 props,且部分 props 在大多数情况下都使用相同的值时,defaultProps 可以极大地简化组件的使用。例如,一个分页组件可能有 pageSize(每页显示数量)、currentPage(当前页码)等 props。通常情况下,我们可能希望 pageSize 默认为 10。

function Pagination(props) {
  return (
    <div>
      Page {props.currentPage} of {props.totalPages}, Page Size: {props.pageSize}
    </div>
  );
}

Pagination.defaultProps = {
  pageSize: 10
};

在使用 Pagination 组件时,开发者如果不需要修改 pageSize,就无需每次都传递该属性,使代码更加简洁。

function App() {
  return <Pagination currentPage={1} totalPages={10} />;
}

(三)向后兼容

在对组件进行升级时,可能会添加新的 props。为了保证老代码依然能够正常运行,我们可以为新添加的 props 设置默认值。假设我们有一个 Button 组件,最初只有 text(按钮文本)属性。

function Button(props) {
  return <button>{props.text}</button>;
}

后来,我们为了增强按钮的样式定制性,添加了一个 variant(按钮变体,如 primarysecondary 等)属性。为了不破坏已有的使用代码,我们可以设置 defaultProps

function Button(props) {
  return <button className={props.variant}>{props.text}</button>;
}

Button.defaultProps = {
  variant: 'default'
};

这样,老代码中使用 <Button text="Click me" /> 依然能够正常工作,同时新代码可以通过传递 variant 属性来定制按钮样式。

(四)组件库开发

在开发 React 组件库时,defaultProps 尤为重要。组件库的使用者可能来自不同的项目背景,他们不一定对每个组件的所有 props 都有深入了解。通过设置合理的 defaultProps,可以降低组件的使用门槛。例如,一个表单输入框组件库中的 Input 组件,可能有 type(输入类型,如 textpassword 等)、placeholder(占位文本)等属性。

function Input(props) {
  return <input type={props.type} placeholder={props.placeholder} />;
}

Input.defaultProps = {
  type: 'text',
  placeholder: 'Enter value'
};

这样,组件库的使用者在快速使用 Input 组件时,无需关心这些细节,就能得到一个基本可用的输入框。

(五)配合 PropTypes 进行类型检查

PropTypes 是 React 提供的一种用于 props 类型检查的工具。虽然从 React v15.5 开始,官方推荐使用 prop-types 库进行类型检查,但 defaultPropsPropTypes 配合使用可以让代码更加健壮。例如:

import PropTypes from 'prop-types';

function Message(props) {
  return <div>{props.text}</div>;
}

Message.propTypes = {
  text: PropTypes.string.isRequired
};

Message.defaultProps = {
  text: 'Default message'
};

在上述代码中,PropTypes 确保 text 属性是字符串类型且是必需的。而 defaultProps 则在没有传递 text 属性时提供一个默认值。这样,在开发过程中,如果不小心遗漏了 text 属性,既会有默认值保证组件正常渲染,又会在开发环境中收到类型检查的警告,提示缺少必要的 props

(六)用于条件渲染中的默认行为

在组件中,有时我们会根据 props 的值进行条件渲染。当 props 不存在时,defaultProps 可以为条件渲染提供默认的行为。比如一个 Tab 组件,根据 active 属性判断是否为激活状态来渲染不同的样式。

function Tab(props) {
  const tabClassName = props.active? 'tab active' : 'tab';
  return <div className={tabClassName}>{props.label}</div>;
}

Tab.defaultProps = {
  active: false
};

这样,在使用 Tab 组件时,如果没有传递 active 属性,它将以非激活状态渲染,符合一般的使用预期。

(七)在受控与非受控组件切换时提供默认值

在 React 中,表单组件分为受控组件和非受控组件。有时我们可能需要在两者之间切换,defaultProps 可以在这种情况下提供便利。例如,一个 Textarea 组件,既可以作为受控组件接收 value 属性,也可以作为非受控组件使用 defaultValue

import React, { Component } from'react';

class TextareaComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: props.defaultValue
    };
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    if (this.props.value === undefined) {
      this.setState({ value: e.target.value });
    }
  }

  render() {
    const inputProps = {
     ...this.props,
      value: this.props.value === undefined? this.state.value : this.props.value,
      onChange: this.handleChange
    };
    return <textarea {...inputProps} />;
  }
}

TextareaComponent.defaultProps = {
  defaultValue: ''
};

在上述代码中,defaultProps 中的 defaultValue 为非受控模式下的 Textarea 提供了初始值,并且在受控与非受控模式切换时起到了关键作用。

(八)在复杂组件组合中提供默认配置

当组件是由多个子组件组合而成,并且这些子组件有一些默认的配置需求时,defaultProps 可以统一管理这些默认配置。比如一个 Card 组件,它内部包含一个 Title 子组件和一个 Content 子组件,Title 子组件可能有默认的字号等样式配置。

function Title(props) {
  return <h2 style={{ fontSize: props.fontSize }}>{props.text}</h2>;
}

Title.defaultProps = {
  fontSize: '18px'
};

function Content(props) {
  return <p>{props.text}</p>;
}

function Card(props) {
  return (
    <div>
      <Title text={props.title} />
      <Content text={props.content} />
    </div>
  );
}

这样,在使用 Card 组件时,无需每次都为 Title 子组件的 fontSize 进行配置,除非有特殊需求。

四、defaultProps 的注意事项

(一)性能考虑

虽然 defaultProps 非常方便,但在某些性能敏感的场景下,需要谨慎使用。每次组件重新渲染时,即使 props 没有变化,defaultProps 也会被重新计算。例如,如果 defaultProps 中的某个值是通过一个复杂函数计算得出的,这可能会导致不必要的性能开销。

function ExpensiveCalculation() {
  // 模拟一个复杂的计算
  let result = 0;
  for (let i = 0; i < 1000000; i++) {
    result += i;
  }
  return result;
}

function MyComponent(props) {
  return <div>{props.value}</div>;
}

MyComponent.defaultProps = {
  value: ExpensiveCalculation()
};

在上述代码中,每次 MyComponent 渲染时,ExpensiveCalculation 函数都会被调用,这显然是不合理的。为了解决这个问题,可以将复杂计算提取到一个单独的变量,并在组件外部计算一次。

const expensiveValue = ExpensiveCalculation();

function MyComponent(props) {
  return <div>{props.value}</div>;
}

MyComponent.defaultProps = {
  value: expensiveValue
};

(二)与 state 的区别

defaultPropsstate 都可以用于为组件提供初始值,但它们有着本质的区别。defaultProps 是用于设置 props 的默认值,props 是从父组件传递过来的,是不可变的。而 state 是组件内部的状态,用于存储组件自身变化的数据,并且可以通过 setState 方法进行更新。例如,一个 Toggle 组件,isOn 既可以通过 props 传递,也可以作为组件的 state

// 使用 props 传递初始值
function Toggle(props) {
  return <button>{props.isOn? 'On' : 'Off'}</button>;
}

Toggle.defaultProps = {
  isOn: false
};

// 使用 state 管理状态
class ToggleClass extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isOn: false
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(prevState => ({
      isOn:!prevState.isOn
    }));
  }

  render() {
    return <button onClick={this.handleClick}>{this.state.isOn? 'On' : 'Off'}</button>;
  }
}

在第一个例子中,isOn 是通过 defaultProps 设置的,组件无法自行改变它。而在第二个例子中,isOn 作为 state,组件可以通过点击按钮来更新它。

(三)动态 defaultProps

在 React 中,defaultProps 本身是静态的,不能根据组件的其他 props 或者 state 动态设置。如果有动态设置默认值的需求,通常需要在组件内部进行逻辑处理。例如,一个 Image 组件,希望根据屏幕宽度来设置默认的图片尺寸。

function Image(props) {
  let width;
  let height;
  if (typeof window!== 'undefined') {
    const screenWidth = window.innerWidth;
    if (screenWidth > 768) {
      width = 400;
      height = 300;
    } else {
      width = 200;
      height = 150;
    }
  }
  const imgWidth = props.width? props.width : width;
  const imgHeight = props.height? props.height : height;
  return <img src={props.src} width={imgWidth} height={imgHeight} />;
}

在上述代码中,虽然不能直接使用 defaultProps 实现动态默认值,但通过在组件内部的逻辑判断,可以达到类似的效果。

(四)对组件可测试性的影响

在进行单元测试时,defaultProps 可能会影响测试的准确性。如果测试中没有明确传递 props,组件将使用 defaultProps 的值。这可能导致一些隐藏的问题在测试中无法暴露。例如,一个 Button 组件的点击事件依赖于 props 中的某个特定值,如果在测试中没有传递该 props 而使用了 defaultProps,可能会使点击事件的测试结果不准确。

function Button(props) {
  const handleClick = () => {
    if (props.isSubmit) {
      // 执行提交逻辑
    }
  };
  return <button onClick={handleClick}>{props.text}</button>;
}

Button.defaultProps = {
  isSubmit: false
};

在测试 Button 组件的点击事件时,如果没有传递 isSubmit: true,点击事件中的提交逻辑将不会被触发,可能会掩盖潜在的问题。因此,在测试时,尽量明确传递 props,避免依赖 defaultProps

五、在不同 React 版本中的变化

(一)早期版本

在 React 的早期版本中,defaultProps 是一个相对简单直接的功能,用于为组件的 props 设置默认值。当时 React 的生态和功能还没有如今这么丰富,defaultProps 主要就是满足组件在缺少某些 props 时能够有合理的默认表现。

(二)与 ES6 类结合的发展

随着 ES6 类在 JavaScript 中的广泛应用,React 对类组件的支持也不断完善。在类组件中使用 defaultProps 作为静态属性的方式变得更加普遍和标准化。这使得代码结构更加清晰,同时也与 ES6 类的语法特性相契合。例如:

import React, { Component } from'react';

class MyClassComponent extends Component {
  render() {
    return <div>{this.props.value}</div>;
  }
}

MyClassComponent.defaultProps = {
  value: 'Default value'
};

这种方式在 React 一段时间内成为了类组件设置 defaultProps 的标准写法。

(三)函数式组件与 Hooks 时代

随着 React Hooks 的出现,函数式组件的功能得到了极大的增强。虽然函数式组件设置 defaultProps 的基本方式没有改变,依然是通过 Component.defaultProps 的形式,但在结合 Hooks 的复杂场景下,defaultProps 的使用需要更多地考虑与 useStateuseEffect 等 Hooks 的协同工作。例如,在一个使用 useState 来管理组件内部状态并且依赖 props 默认值的函数式组件中:

import React, { useState } from'react';

function MyFunctionalComponent(props) {
  const [localValue, setLocalValue] = useState(props.defaultValue);
  return <div>{localValue}</div>;
}

MyFunctionalComponent.defaultProps = {
  defaultValue: 'Initial value'
};

这里 defaultPropsuseState 提供了初始值,在这种情况下,需要注意 props 的变化以及 defaultPropsuseState 之间的同步关系,以确保组件的正确行为。

(四)未来可能的发展

虽然目前 defaultProps 的基本功能和使用方式相对稳定,但随着 React 框架的不断演进,可能会在以下方面有所改进。例如,可能会提供更便捷的方式来处理动态默认值,或者在类型检查与 defaultProps 的集成上有更好的体验。也许未来会有更高级的语法糖,让开发者能够更优雅地设置和管理 props 的默认值,同时提升代码的可读性和可维护性。

六、对比其他前端框架类似功能

(一)Vue.js 的 props 默认值设置

在 Vue.js 中,组件的 props 也可以设置默认值。Vue 通过在 props 选项中定义对象来设置默认值。例如:

<template>
  <div>{{ message }}</div>
</template>

<script>
export default {
  props: {
    message: {
      type: String,
      default: 'Default message'
    }
  }
};
</script>

与 React 的 defaultProps 相比,Vue 的方式将类型定义和默认值设置结合在一起,在一个对象中完成。而 React 通常会借助 PropTypes 库来进行类型检查,defaultProps 单独设置默认值。这种差异使得 Vue 的写法在设置 props 时更加紧凑,而 React 的方式在类型检查和默认值设置上相对分离,更具灵活性。

(二)Angular 的组件输入属性默认值

在 Angular 中,组件通过 @Input() 装饰器来定义输入属性。要设置默认值,可以在组件类的构造函数中进行初始化。例如:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-my-component',
  templateUrl: './my - component.html'
})
export class MyComponent {
  @Input() message = 'Default message';
}

在模板中使用该组件时,如果没有传递 message 属性,将使用默认值。Angular 的这种方式直接在组件类中初始化输入属性的默认值,与 React 和 Vue 都有所不同。它利用了 TypeScript 的类属性初始化特性,简洁明了,但在处理复杂的默认值逻辑时,可能不如 React 通过 defaultProps 在组件外部统一设置那么灵活。

(三)Svelte 的组件属性默认值

在 Svelte 中,组件属性的默认值通过在组件脚本中直接赋值来实现。例如:

<script>
  let name = 'Guest';
  export let message;
  if (!message) {
    message = 'Welcome, {name}';
  }
</script>

<p>{message}</p>

Svelte 的这种方式相对比较直观,在组件内部通过简单的逻辑判断来设置属性的默认值。与 React 相比,它没有像 defaultProps 那样专门的机制,而是更倾向于在组件内部进行属性的初始化和默认值处理,这使得 Svelte 的组件在处理默认值时更加贴近原生 JavaScript 的逻辑方式。

通过对比不同前端框架类似功能,可以发现 React 的 defaultProps 在设计上有着自身的特点和优势,它与 React 的组件化架构以及生态系统紧密结合,为开发者提供了一种灵活且有效的设置 props 默认值的方式。同时,了解其他框架的类似功能也有助于我们在不同场景下选择最适合的前端框架进行开发。