TypeScript + React:为什么我不使用React.FC?

4,231 阅读4分钟

在我最近关于TypeScript和React组件模式的文章中,我已经说过,我不使用内置的类型React.FC<> ,而是用类型化的孩子来明确。我没有说明原因,而这引发了一些兴趣。这就是为什么我想详细说明一下。

请注意,这只是一个观点,而不是一个建议或其他什么。如果你喜欢使用React.FC ,而且它对你有效,请继续这样做!这完全没问题,有时我也会这样做!让我们严肃一点:外面有不同的问题需要讨论,更足以成为生气的理由。所以,不要浪费你的时间对代码风格过于激动。但如果你喜欢获得新的想法,请继续吧

Martin Hochel致敬,他在很久以前在他的关于组件模式的文章中写到不要使用React.FC

什么是React.FC<>?#

在React中,你有两种定义组件的方法。

  1. 编写一个类并扩展自Component
  2. 编写一个函数并返回JSX

由于React不是用TypeScript编写的,社区提供了类型与@types/react 包。在里面有一个通用的类型,叫做FC ,允许我们对我们的函数组件进行类型化,就像这样。

import React, { FC } from "react";

type GreetingProps = {
  name: string;
}

const Greeting:FC<GreetingProps> = ({ name }) => {
  // name is string!
  return <h1>Hello {name}</h1>
};

我个人认为这个类型非常好,因为它只用几行代码就涵盖了函数组件可以和允许的一切

interface FunctionComponent<P = {}> {
  (props: PropsWithChildren<P>, context?: any)
    : ReactElement<any, any> | null;
  propTypes?: WeakValidationMap<P>;
  contextTypes?: ValidationMap<any>;
  defaultProps?: Partial<P>;
  displayName?: string;
}

参考GitHub。尽管如此,我还是没有使用它。那么为什么呢?

1.你输入的是一个函数,而不是它的参数#

React.FC 键入一个函数。那是在它的名字中,函数组件。函数类型真的很难适用于常规命名的函数。在这样的代码中,你要把函数类型放在哪里。

function Greeting({ name }) {
  return <h1>Hello {name}</h1>
}

你可以使用一个匿名函数,并把它赋给一个常量/变量。

const Greeting:FC<GreetingProps> = function({ name }) {
  return <h1>Hello {name}</h1>
}

或者像上面的例子那样使用箭头函数。但我们完全排除了简单的、常规的函数。

如果我们不对函数进行分类,而是对它的属性进行分类,我们可以使用任何形式的函数来实现我们的目标。

// ✅
function Greeting({ name }: GreetingProps) {
  return <h1>Hello {name}</h1>
}

是的,即使在箭头函数和转置的时代,写普通的、老的、无聊的、能用的、简单的命名的函数也是完全有效的!而且很好!

2.FC<>总是隐含着子类#

这也是Martin在他的原始文章中提出的一个论点。用React.FC<> 打字的时候,你的组件会为子代打开。比如说。

export const Greeting:FC<GreetingProps> = ({ name }) => {
  // name is string!
  return <h1>Hello {name}</h1>
};

// use it in the app
const App = () => <>
  <Greeting name="Stefan">
    <span>{"I can set this element but it doesn't do anything"}</span>
  </Greeting>
</>

如果我使用简单的props而不是FC ,TypeScript就会告诉我,即使我的组件告诉我不应该传给孩子。

function Greeting({ name }: GreetingProps) {
  return <h1>Hello {name}</h1>
}
const App = () => <>
  <Greeting name="Stefan">
    {/* The next line throws errors at me! 💥*/}
    <span>{"I can set this element but it doesn't do anything"}</span>
  </Greeting>
</>

而且公平地说,处理子类型不是TypeScript的强项。但至少在一开始就获得不应该有孩子的信息是有帮助的。

要明确。说明你的组件在真正需要的时候使用子类型我为此写了一个WithChildren 的辅助类型。

type WithChildren<T = {}> = 
  T & { children?: React.ReactNode };

type CardProps = WithChildren<{
  title: string
}>

function Card({ title, children }: CardProps) {
  return <>
    <h1>{ title }</h1>
    {children}
  </>
}

它的效果一样好,而且有一个很大的好处...

3.更容易转移到Preact#

如果你没有使用Preact,你应该使用!它做同样的事情,preact/compat 包确保你与React生态系统兼容。你可以节省多达100KB的生产规模,而且你使用的是一个独立的库

最近我开始将我所有的React项目转移到Preact。Preact是用TypeScript写的(有JSDoc注释),所以你安装了Preact就能得到所有好的类型信息。来自@types/react 的一切都不再兼容了。由于React.FC 是附加的,你需要重构你所有的现有代码,使其无论如何都能模仿带有类型化道具的简单函数。

// The Preact version
type WithChildren<T = {}> = 
  T & { children?: VNode };

4.4.React.FC<>破坏了defaultProps#

defaultProps 是基于类的React的遗留问题,在那里你可以为你的props设置默认值。有了函数组件,现在这就是基本的JavaScript,你可以在这里看到。然而,你可能会遇到一些情况,设置 (或其他静态属性)仍然是必要的。defaultProps

从3.1版本开始,TypeScript有一个机制来理解defaultProps ,并且可以根据你设置的值来设置默认值。然而,React.FC 类型为defaultProps ,因此打破了将它们作为默认值的连接。所以这个就断了。

export const Greeting:FC<GreetingProps> = ({ name }) => {
  // name is string!
  return <h1>Hello {name}</h1>
};

Greeting.defaultProps = {
  name: "World"
};

const App = () => <>
  {/* Big boom 💥*/}
  <Greeting />
</>

不使用FC ,而只是一个带有类型化道具的函数(常规的、命名的、匿名的、箭头的,等等)就可以了

export const Greeting = ({ name }: GreetingProps) => {
  // name is string!
  return <h1>Hello {name}</h1>
};

Greeting.defaultProps = {
  name: "World"
};

const App = () => <>
  {/* Yes! ✅ */}
  <Greeting />
</>

5.面向未来的#

还记得大家把函数组件称为无状态函数组件的时候吗?是的,随着钩子的引入,我们的函数组件中突然有了很多状态。这反映在现在的SFC 类型上,FC ,谁知道呢,将来也会改变。将你的参数(props)而不是函数本身输入,可以让你远离改变函数类型。

底线#

所以这些是我不使用React.FC 的几个论据。也就是说,我认为使用React.FC ,如果它对你的工作流程有好处,是完全合法的,也是可以的。我在过去一直在使用它,而且,它很有效!所以,请不要觉得自己被逼无奈。所以,如果你能用它来运行,请不要觉得被逼着去改变你的编码风格。

但也许你会发现,输入道具可以更简单,更接近于JavaScript。而这也许更适合你的编码风格!