使用 React Hooks 和 TypeScript
已发表: 2022-03-11Hooks 于 2019 年 2 月被引入 React,以提高代码的可读性。 我们已经在之前的文章中讨论了 React 钩子,但这次我们正在研究钩子如何与 TypeScript 一起工作。
在 hooks 之前,React 组件曾经有两种风格:
- 处理状态的类
- 完全由它们的 props 定义的函数
这些的自然用途是构建具有类的复杂容器组件和具有纯函数的简单表示组件。
什么是 React Hooks?
容器组件处理状态管理和对服务器的请求,本文将在副作用中调用这些组件。 状态将通过 props 传播到容器子级。
但是随着代码的增长,功能组件往往会被转换为容器组件。
将功能组件升级为更智能的组件并没有那么痛苦,但这是一项耗时且令人不快的任务。 此外,不再欢迎非常严格地区分演示者和容器。
Hooks 可以两者兼得,因此生成的代码更加统一,并且具有几乎所有的好处。 这是一个将本地状态添加到处理引用签名的小组件的示例。
// put signature in local state and toggle signature when signed changes function QuotationSignature({quotation}) { const [signed, setSigned] = useState(quotation.signed); useEffect(() => { fetchPost(`quotation/${quotation.number}/sign`) }, [signed]); // effect will be fired when signed changes return <> <input type="checkbox" checked={signed} onChange={() => {setSigned(!signed)}}/> Signature </> }
这有一个很大的好处——TypeScript编码在 Angular 中很棒,但在 React 中显得臃肿。 然而,使用 TypeScript 编写 React 钩子是一种愉快的体验。
带有旧 React 的 TypeScript
TypeScript 是由微软设计的,并且在 React 开发Flow时遵循了 Angular 的路径,但现在已经失去了吸引力。 用朴素的 TypeScript 编写 React 类是相当痛苦的,因为即使许多键相同,React 开发人员也必须同时输入props
和state
。
这是一个简单的域对象。 我们制作了一个报价应用程序,具有Quotation
类型,在一些带有状态和道具的 crud 组件中进行管理。 可以创建Quotation
单,其关联状态可以更改为已签名或未签名。
interface Quotation{ id: number title:string; lines:QuotationLine[] price: number } interface QuotationState{ readonly quotation:Quotation; signed: boolean } interface QuotationProps{ quotation:Quotation; } class QuotationPage extends Component<QuotationProps, QuotationState> { // ... }
但是想象一下 QuotationPage 现在将向服务器询问一个id :例如,公司的第 678 条报价。 好吧,这意味着 QuotationProps 不知道那个重要的数字——它没有完全包装一个 Quotation 。 我们必须在 QuotationProps 接口中声明更多代码:
interface QuotationProps{ // ... all the attributes of Quotation but id title:string; lines:QuotationLine[] price: number }
我们将除 id 之外的所有属性复制到一个新类型中。 嗯。 这让我想起了旧的 Java,它涉及编写一堆 DTO。 为了克服这个问题,我们将增加我们的 TypeScript 知识以绕过痛苦。
带有 Hooks 的 TypeScript 的好处
通过使用钩子,我们将能够摆脱之前的 QuotationState 接口。 为此,我们将 QuotationState 拆分为状态的两个不同部分。
interface QuotationProps{ quotation:Quotation; } function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }
通过拆分状态,我们不必创建新接口。 本地状态类型通常由默认状态值推断。
带有钩子的组件都是函数。 因此,我们可以编写返回 React 库中定义的FC<P>
类型的相同组件。 该函数显式声明其返回类型,并沿 props 类型设置。
const QuotationPage : FC<QuotationProps> = ({quotation}) => { const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }
显然,使用带有 React 钩子的 TypeScript 比使用 React 类更容易。 而且因为强类型对于代码安全来说是一种有价值的安全措施,所以如果你的新项目使用钩子,你应该考虑使用 TypeScript。 如果你想要一些 TypeScript,你绝对应该使用钩子。
有很多原因可以避免使用 TypeScript,无论是否使用 React。 但是,如果您确实选择使用它,那么您也绝对应该使用钩子。
适用于 Hooks 的 TypeScript 的具体功能
在前面的 React hooks TypeScript 示例中,我在 QuotationProps 中仍然有 number 属性,但目前还不知道这个数字到底是什么。
TypeScript 为我们提供了一长串实用程序类型,其中三个将通过减少许多接口描述的噪音来帮助我们使用 React。
-
Partial<T>
: T 的任何子键 Omit<T, 'x'>
: T 的所有键,除了键x
-
Pick<T, 'x', 'y', 'z'>
: 正是来自T
的x, y, z
键
在我们的例子中,我们希望Omit<Quotation, 'id'>
省略引用的 id。 我们可以使用type
关键字动态创建一个新类型。
Partial<T>
和Omit<T>
在大多数类型化语言(如 Java)中不存在,但对于前端开发中的 Forms 示例有很大帮助。 它简化了打字的负担。

type QuotationProps= Omit<Quotation, id>; function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); // ... }
现在我们有一个没有 id 的报价单。 所以,也许我们可以设计一个Quotation
和PersistedQuotation
extends
Quotation
。 此外,我们将轻松解决一些经常出现的if
或undefined
问题。 我们还应该调用变量引用吗,尽管它不是完整的对象? 这超出了本文的范围,但我们稍后会提到它。
但是,现在我们确信我们不会传播我们认为有number
的对象。 使用Partial<T>
并不能带来所有这些保证,因此请谨慎使用。
Pick<T, 'x'|'y'>
是另一种动态声明类型的方法,无需声明新接口。 如果是组件,只需编辑引用标题:
type QuoteEditFormProps= Pick<Quotation, 'id'|'title'>
要不就:
function QuotationNameEditor({id, title}:Pick<Quotation, 'id'|'title'>){ ...}
不要评判我,我是领域驱动设计的忠实粉丝。 我并不懒惰到不想为新界面多写两行。 我使用接口来精确描述域名,这些实用函数用于本地代码正确性,避免噪音。 读者会知道Quotation
是规范的接口。
React Hooks 的其他好处
React 团队始终将 React 视为一个功能框架。 他们使用类以便组件可以处理自己的状态,现在挂钩作为一种允许函数跟踪组件状态的技术。
interface Place{ city:string, country:string } const initialState:Place = { city: 'Rosebud', country: 'USA' }; function reducer(state:Place, action):Partial<Place> { switch (action.type) { case 'city': return { city: action.payload }; case 'country': return { country: action.payload }; } } function PlaceForm() { const [state, dispatch] = useReducer(reducer, initialState); return ( <form> <input type="text" name="city" onChange={(event) => { dispatch({ type: 'city',payload: event.target.value}) }} value={state.city} /> <input type="text" name="country" onChange={(event) => { dispatch({type: 'country', payload: event.target.value }) }} value={state.country} /> </form> ); }
这是一个使用Partial
是安全且不错的选择的案例。
虽然一个函数可以被多次执行,但关联的useReducer
钩子只会被创建一次。
通过自然地从组件中提取reducer函数,代码可以分成多个独立的函数,而不是一个类内部的多个函数,都与类内部的状态相关联。
这显然对可测试性更好——一些函数处理 JSX,其他处理行为,其他处理业务逻辑,等等。
你(几乎)不再需要高阶组件了。 使用函数编写渲染道具模式更容易。
因此,阅读代码更容易。 您的代码不是类/函数/模式流,而是函数流。 但是,由于您的函数未附加到对象,因此可能很难命名所有这些函数。
TypeScript 仍然是 JavaScript
JavaScript 很有趣,因为您可以从任何方向撕毁您的代码。 使用 TypeScript,您仍然可以使用keyof
来处理对象的键。 你可以使用类型联合来创建一些不可读和不可维护的东西——不,我不喜欢那些。 您可以使用类型别名来假装字符串是 UUID。
但是你可以在零安全的情况下做到这一点。 确保您的tsconfig.json
具有"strict":true
选项。 在项目开始之前检查它,否则你将不得不重构几乎每一行!
关于您在代码中输入的级别存在争议。 您可以键入所有内容或让编译器推断类型。 这取决于 linter 配置和团队选择。
此外,您仍然可以犯运行时错误! TypeScript 比 Java 更简单,并且避免了泛型的协变/逆变问题。
在此 Animal/Cat 示例中,我们有一个与 Cat 列表相同的 Animal 列表。 不幸的是,这是第一行的合同,而不是第二行。 然后,我们将一只鸭子添加到 Animal 列表中,因此 Cat 的列表为 false。
interface Animal {} interface Cat extends Animal { meow: () => string; } const duck = {age: 7}; const felix = { age: 12, meow: () => "Meow" }; const listOfAnimals: Animal[] = [duck]; const listOfCats: Cat[] = [felix]; function MyApp() { const [cats , setCats] = useState<Cat[]>(listOfCats); // Here the thing: listOfCats is declared as a Animal[] const [animals , setAnimals] = useState<Animal[]>(listOfCats) const [animal , setAnimal] = useState(duck) return <div onClick={()=>{ animals.unshift(animal) // we set as first cat a duck ! setAnimals([...animals]) // dirty forceUpdate } }> The first cat says {cats[0].meow()}</div>; }
TypeScript 只有一种用于泛型的双变量方法,这种方法很简单,有助于 JavaScript 开发人员采用。 如果您正确命名变量,您很少会在listOfCats
中添加duck
。
此外,还有一个关于添加和输出协变和逆变合同的建议。
结论
我最近回到了 Kotlin,它是一种很好的类型化语言,正确键入复杂的泛型是很复杂的。 由于鸭子类型和双变量方法的简单性,您没有使用 TypeScript 的一般复杂性,但是您还有其他问题。 也许你在使用 TypeScript 和为 TypeScript 设计的 Angular 上没有遇到很多问题,但是你在 React 类中遇到了这些问题。
TypeScript 可能是 2019 年的大赢家。它获得了 React,但也凭借 Node.js 及其使用非常简单的声明文件键入旧库的能力征服了后端世界。 它埋葬了 Flow,尽管有些与 ReasonML 一路走来。
现在,我觉得 hooks+TypeScript 比 Angular 更令人愉快和高效。 六个月前我不会这么想。