刺猬的开发笔记之一文讲明白React.FC是什么

1. React.FC是一个函数式组件,是在TypeScript中使用的一个泛型

(很神奇,看了很多篇文章开头的介绍都是这两句)
React.FC 包含了 PropsWithChildren 的泛型,不用显式的声明 props.children 的类型。
这个时候我的疑问就冒出来了
首先,PropsWithChildren是什么?其次,React.FC是怎么包含PropsWithChildren 的泛型的?
让我们逐一解答

1.1 PropsWithChildren 详解

PropsWithChildren 是 React 提供的一个工具类型,用于为组件 Props 自动添加 children 属性。源码长这个样子:

type PropsWithChildren<P> = P & {
  children?: React.ReactNode;
};

这里使用了&符号将任意的Props类型P和children属性合并,children是可选属性,类型为React.ReactNode,也就是任意合法的React子元素。
举个简单的属性合并的例子我们就能知道他到底做了什么了:

// 原始 Props 类型
interface UserProps {
  name: string;
  age: number;
}

// 应用 PropsWithChildren
type UserWithChildren = PropsWithChildren<UserProps>;

/* 合并后等价于:
type UserWithChildren = {
  name: string;
  age: number;
  children?: React.ReactNode;
} */

const props: UserWithChildren = {
  name: "Alice",
  age: 30,
  children: <div>Hello</div> // ✅ 合法
};

const props2: UserWithChildren = {
  name: "Bob",
  age: 25 // ✅ 合法(children 可选)
};

当原类型P包含children属性时,则会出现属性覆盖

interface ConflictProps {
  children: string; // 已定义 children 为 string
}

type Merged = PropsWithChildren<ConflictProps>;

/* 合并后等价于:
type Merged = {
  children: string & React.ReactNode; // 类型冲突!
} */

// string 是 React.ReactNode 的子类型(因为 React.ReactNode 包含字符串)最终 children 类型为 string(更具体的类型胜出)但此时 children 变为可选属性(因为交叉类型合并了可选性)

const props: Merged = {
  children: "text" // ✅ 合法(string 是合法 ReactNode)
};

const props2: Merged = {
  children: <div/> // ❌ 错误:JSX 元素不是 string
};

const props3: Merged = {}; // ❌ 错误:原类型要求 children 必填

1.2 React.FC 的泛型集成原理

React 源码中 React.FC 的定义如下:

typescript
type FC<P = {}> = FunctionComponent<P>;

interface FunctionComponent<P = {}> {
  (props: PropsWithChildren<P>, context?: any): ReactElement | null;
  // 其他静态属性...
}

用户传入 Props 类型 P,React.FC 将 P 传递给 PropsWithChildren

,最终得到的 props 类型为 P & { children?: ReactNode }
示例代码如下:

const MyComponent: React.FC<{ title: string }> = (props) => {
  // props 自动获得 children 属性
  return <div>{props.title}{props.children}</div>;
};

相当于

// 步骤 1:用户传入 P = { title: string }
type Props = { title: string };

// 步骤 2:应用 PropsWithChildren
type FinalProps = PropsWithChildren<Props> = {
  title: string;
  children?: React.ReactNode;
};

// 步骤 3:组件函数签名变为
(props: FinalProps) => ReactElement | null;

最后整体出来的效果,我们看React.FC和普通函数组件的对比就能很明显地看出区别了。

// 使用 React.FC
const ComponentA: React.FC<{ title: string }> = ({ title, children }) => (
  <div>{title} {children}</div>
);

// 普通函数组件
const ComponentB = ({ title, children }: { 
  title: string; 
  children?: React.ReactNode // 需要显式声明
}) => <div>{title} {children}</div>;

2. React.FC<> 对于返回类型是显式的,而普通函数版本是隐式的(否则需要附加注释)。

// React.FC 强制返回类型为 ReactElement | null
const ValidComponent: React.FC = () => <div>Hello</div>; // ✅
const InvalidComponent: React.FC = () => "text"; // ❌ 返回字符串不符合要求

ReactElement | null 是 React 函数组件(Function Component)的 合法返回类型,它定义了组件可以返回的有效内容类型。这是 React 类型系统的核心约束之一,用于确保组件渲染行为的类型安全。
为什么,因为ReactElement本质上是JSX语法或 React.createElement() 创建的 虚拟 DOM 对象,null表示组件不渲染任何内容,而React函数组件的核心职责就是返回描述UI的React元素。

// JSX 编译后:
const element = <div>Hello</div>;
// 等价于:
const element = React.createElement('div', null, 'Hello');

以下对比就可以清晰地看出对于函数组件来说上述优化的优势

// 无类型约束
const Component = () => {
  return "text"; // ✅ 不会报错(但运行时可能出错)
};
const Component: React.FC = () => {
  return "text"; // ❌ 编译时立即报错
};

3. 使用React.FC写 React 组件的时候,不能用setState,取而代之的是useState()、useEffect等 Hook API。

在 React 中,setState 是类组件特有的状态更新方法,而函数组件使用 useState Hook 返回的 setter 函数。
setState

import React, { Component } from 'react';

class ClassCounter extends Component {
  state = { count: 0 }; // 初始化状态

  // 使用 setState 更新状态
  increment = () => {
    this.setState({ count: this.state.count + 1 }); // ✅ 对象式更新
  };

  decrement = () => {
    this.setState(prevState => ({ // ✅ 函数式更新(依赖前值)
      count: prevState.count - 1 
    }));
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>+</button>
        <button onClick={this.decrement}>-</button>
      </div>
    );
  }
}

useState

import React, { useState } from 'react';

const FunctionCounter: React.FC = () => {
  const [count, setCount] = useState(0); // 初始化状态

  // 使用 setCount 更新状态
  const increment = () => {
    setCount(count + 1); // ✅ 直接更新
  };

  const decrement = () => {
    setCount(prevCount => prevCount - 1); // ✅ 函数式更新
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
};

React 16.8 前,函数组件是一个纯函数,纯函数仅接收 props并返回JSX,无生命周期/状态管理,是无状态组件。
React 16.8+版本React.FC成为了一个函数式组件,引入了Hooks之后,可通过 useState 管理状态并通过 useEffect 处理副作用,是有状态组件,也不再是严格纯函数了。

// 使用 React.FC 声明 + Hooks 管理状态
const Counter: React.FC = () => {
  const [count, setCount] = useState(0); // ✅ 完全合法

  return <button onClick={() => setCount(c => c+1)}>{count}</button>;
}

4. React.FC提供了类型检查和自动完成的静态属性:displayName,propTypes和defaultProps(注意:defaultProps与React.FC结合使用会存在一些问题)。

4.1 displayName(调试标识)

// 定义一个匿名箭头函数
const MyComponent: React.FC = () => <div>Hello</div>;

// 类型自动包含 displayName
MyComponent.displayName = "MyAwesomeComponent"; // ✅ 类型检查通过

// React DevTools 显示组件名称:
// <MyAwesomeComponent> 而不是 <Anonymous>
// <Anonymous> 是 React DevTools 用来表示无法确定名称的组件的占位符名称

4.2 propTypes(运行时类型检查)

相当于提供了双重保障,TypeScript编译时也会报错,propTypes运行时也会进行类型检查。

import PropTypes from 'prop-types';

const Greeting: React.FC<{ name: string }> = ({ name }) => (
  <h1>Hello, {name}</h1>
);

// 类型自动包含 propTypes
Greeting.propTypes = {
  name: PropTypes.string.isRequired // ✅ 类型匹配
};

// 错误示例(TS 会报错):
Greeting.propTypes = {
  name: PropTypes.number // ❌ 类型不匹配
};

最佳实践是,在 TypeScript 项目中,优先依赖静态类型,仅在需要与 JavaScript 生态交互时补充 PropTypes。两者结合为组件提供从开发到运行的全周期类型安全。

4.3 defaultProps(默认属性值)

以下是ES6默认参数和defaultProps的区别对比

interface Props {
  size?: 'sm' | 'md' | 'lg';
}

// ES6默认参数
const Button: React.FC<Props> = ({ size = 'md' }) => (
  <button className={`btn-${size}`}>Click</button>
);

// 类型自动包含 defaultProps
Button.defaultProps = {
  size: 'md' // ✅ 类型匹配
};

问题在于:TypeScript 不会自动将 defaultProps 的默认值注入到 props 类型中。这导致以下矛盾行为:

// 正确方式:ES6 默认参数
const Button: React.FC<Props> = ({ size = 'md' }) => { ... }
// ✅ 使用时:<Button /> → size 自动为 'md'

// 问题方式:使用 defaultProps
Button.defaultProps = { size: 'md' };
// ❌ 使用时:<Button /> → TS 认为 size 是 undefined | 'sm' | 'md' | 'lg'

本来作为一个可选属性 size可以传入 sm、md、lg和undefined,但是有了默认值md,理论上size就不可能是undefined了,es6默认参数可以在这时进行自动的收窄,但是defaultProps不行

// 原始类型:
type Props = { size?: 'sm' | 'md' | 'lg' };

// 解构 + 默认值 等价于:
const size: Props['size'] = props.size ?? 'md';
// => 类型变为 NonNullable<Props['size']> | 'md'
interface Props {
  size?: 'sm' | 'md' | 'lg'; // 可选属性
}

const Button: React.FC<Props> = (props) => {
  // 即使设置了 defaultProps,TS 仍认为 props.size 可能为 undefined
  console.log(props.size); // type: 'sm' | 'md' | 'lg' | undefined ❌
  
  return <button>{props.size}</button>; // 可能渲染 "undefined"!
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值