[译] 使用 TypeScript 开发 React Hooks

简介: [译] 使用 TypeScript 开发 React Hooks

原文:www.toptal.com/react/react…

React hooks 在 2019 年二月被引入,以改善代码可读性。本文将探讨如何将其和 TypeScript 协同使用。

在 hooks 之前,有两种风格的 React 组件:

  • 处理状态的 类组件(Classes)
  • 完全由其 props 定义的 函数式(Functional)组件

一种常见用法是,由前者构建复杂的容器(Container)组件,而后者负责简单些的展示型(Presentational)组件。

何为 React Hooks ?

容器组件负责状态(state)管理,以及在本文中被称为“副作用(side effects)”的远端请求。状态将经由 props 传播到子组件。

但随着代码的增长,函数式组件也大有取代类组件成为容器的意思。

将函数式组件升级为状态庞杂的容器倒是谈不上痛苦,只是费时费力。此外,严格区分所谓容器和展示组件也不那么被看重了。

Hooks 可以很好地兼顾, 能让代码既通用,又拥有几乎所有的优点。这里有个例子,用来演示如何向一个处理报价签署的组件中增添一个本地状态:


// 在一个本地状态中放置签名,并在签署状态改变时切换签名
function QuotationSignature({quotation}) {
   const [signed, setSigned] = useState(quotation.signed);
   useEffect(() => {
       fetchPost(`quotation/${quotation.number}/sign`)
   }, [signed]); // 签署状态改变时,副作用将被触发
   return <>
       <input type="checkbox" checked={signed} 
            onChange={() => {setSigned(!signed)}}/>
       Signature
   </>
}

还有个利好不得不说 -- 虽然相比于 TypeScript  在 Angular 中的丝滑编码,到了 React 中总被诟病臃肿难用;但 用 TypeScript 搭配 React hooks 却变为了一种愉悦的体验。

旧 React 里的 TypeScript

TypeScript 由微软设计并沿着 Angular 的路径一路进发,而彼时 React 开发出的 Flow 已然式微。在 React 类组件中编写原生 TypeScript 着实痛苦,因为 React 开发者不得不同时对 propsstate 定义类型,即便二者的许多属性是相同的。

按原来的方式来说,先得有一个 Quotation 类型,用来管理某些 CRUD 组件的 state 和 props。并在其相关的 state 中,创建一个 Quotation 类型的属性,以及指示已签署或未签署的状态。


interface QuotationLine {
  price: number
  quantity: number
}
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 时:之前定义的 QuotationProps 将无法获知这个关键的数字值 -- 不完整的数据也无法被 Quotation 类型 精确 匹配。我们可能不得不在 QuotationProps 接口中声明更多的代码:


interface QuotationProps{
   // 除去 id 之外 Quotation 中的所有属性:
   title: string;
   lines: QuotationLine[]
   price: number
}

我们拷贝了除去 id 之外的所有属性搞出一个新类型。这...让我回忆起在 Java 中,被不得不编写的一大堆 DTO (译注:Data Transfer Object,数据传输对象 -- 一种不包含业务逻辑的简单容器,其行为限于内部一致性检查和基本验证等) 所支配的恐惧。

为了克服这种痛苦,我们得在 TypeScript 的知识上补补课了。

TypeScript 结合 hooks 的好处

通过使用 hooks,我们就可以摒弃之前的 QuotationState -- 可以将其拆分为不同的两部分:


// ...
interface QuotationProps{
   quotation: Quotation;
}
function QuotationPage({quotation}:QuotationProps) {
   // 译注:由两个 useXXX 函数分摊了之前接口中的两个属性
   const [quotation, setQuotation] = useState(quotation);
   const [signed, setSigned] = useState(false);
   
   // ...
}

通过拆分状态,就省去了明确创建新的接口。本地状态类型往往能推导出默认的状态值。

因为 hooks 组件就是函数,故可以编写返回 React.FC<Props> 类型(译注:FC 即 function components)的相同组件函数。这样的函数显式声明了其函数式组件的返回类型,并明确了 props 类型。


const QuotationPage: FC<QuotationProps> = ({quotation}) => {
   const [quotation, setQuotation] = useState(quotation);
   const [signed, setSigned] = useState(false);
   
   // ...
}

显然,在 React hooks 中使用 TypeScript 比在类组件中容易。并且因为强类型对于代码安全是个有力的保障,如果你的新项目中用了 hooks 就应该考虑采用 TypeScript 了。

适配 hooks 的 TypeScript 特性

在之前的 React hooks TypeScript 例子中,对于 QuotationProps 接口中的属性如何使用、使用哪些,仍是不甚了了、颇有不便。

TypeScript 其实提供了不少“工具方法”,以便在 React 中描述接口时有效“降噪”。

  • Partial<T> : T 类型所有键的任意子集
  • Omit<T, 'x'> : 除 x 之外的 T 类型所有键
  • Pick<T, 'x', 'y', 'z'> : 从 T 类型中明确拾取 x, y, z

在我们的用例中,可以用 Omit<Quotation, 'id'> 的形式来将 id 排除在 Quotation 类型之外。结合 type 关键字反手就能甩出一个新类型。

Partial<T>Omit<T> 并不存在于 Java 等大部分强类型语言中,但常在前端开发中以各种方式大展身手。它们简化了类型定义的负担。


type QuotationProps = Omit<Quotation, id>;
function QuotationPage({quotation}: QuotationProps){
   const [quotation, setQuotation] = useState(quotation);
   const [signed, setSigned] = useState(false);
   
   // ...
}

当然,或许也可以用 extends 更清晰地区分出持久化类型等:


interface Quote{
   title: string;
   lines: QuotationLine[]
   price: number
}
interface PersistedQuote extends Quote{
  id: number;
}

这样在处理相关属性时,也简化了常见的 ifundefined 问题。

慎用 Partial<T>,它基本不会带来任何保障。

Pick<T, ‘x’|’y’> 是另一种不用声明新接口就能随时定义新类型的方式。如果一个组件只需要简单编辑报价标题的话:


type QuoteEditFormProps = Pick<Quotation, 'id'|'title'>

或直接在行内声明:


function QuotationNameEditor({id, title}: Pick<Quotation, 'id'|'title'>){ ...}

别怀疑,我可是领域驱动设计(DDD - Domain Driven Design)的铁杆拥趸。我并不是懒得为了声明个新接口而懒得多写两行 -- 需要精确描述领域内命名时,我会使用接口;而出于保证本地代码正确性、降噪的目的,我就使用这些 TS 工具语法。

React Hooks 的其他益处

React 团队始终将 React 视为一个函数式框架。过去他们使用类组件以处理自身状态,现在有了 hooks 这种允许一个函数跟踪组件状态的技术。


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 的良好用例。

尽管 reducer 函数会被多次执行,但相关的 useReducer hook 将只被创建一次。

通过 自然而然地 将 reducer 函数定义在组件之外,代码可以被分割成多个独立的函数,而不是都集中在一个类中并共同围绕着其内部状态。

这对可测试性大有裨益 -- 某些函数只处理 JSX,其他一些只处理业务逻辑,等等。

你(几乎)不再需要高阶组件(HOC - Higher Order Components)了。渲染属性(render props)模式更易于编写函数式组件。

这样一来,阅读代码变得更容易了。代码不再是连绵混杂的 类/函数/模式,而仅仅是函数的集合。然而,因为这些函数并未附加到一个对象中,对它们命名可能有点难。

TypeScript 仍是 JavaScript

JavaScript 的乐趣在于你能以任何方式摆弄你的代码。加上 TypeScript 后,你仍可以用 keyof 访问对象的所有键,也能使用类型联合创建出晦涩难搞的某些东西 -- 怕了怕了。

要确保你的 tsconfig.json 设置了 "strict":true 选项。在项目动工前就检查它,否则你将不得不重构很多东西!

对于以何种程度类型化代码是有争议的。你可以手动定义所有东西,也可以让编译器推断出类型。这取决于 linter 工具的配置和团队约定。

同时,你仍会遇到运行时错误!TypeScript 比 Java 简单,并且回避了泛型的协变/逆变问题。

在下例中,有一个 Animal 列表,以及一个相同的 Cat 列表。


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);
  // 问题1:再次被使用的 listOfCats 声明为了一个 Animal[]
  const [animals , setAnimals] = useState<Animal[]>(listOfCats)
  const [animal , setAnimal] = useState(duck)
  return <div onClick={()=>{
      animals.unshift(animal) // 问题2:指鸭为猫
      setAnimals([...animals]) // 造成 dirty forceUpdate
    }}>
    The first cat says {cats[0].meow()} // 不言而喻
  </div>;
}

糟糕的是,由于分别用 Cat[]Animal[] 两种泛型声明了 listOfCats,而后把 listOfAnimals 中的 duck 错误地压入了第二次声明为  Animal[] 的 listOfCats 数组 -- 仅从 TS 静态语法上看是一个 Animal 进入了一个 Animal[],但这就让随后对第一次声明为 Cat[] 的 listOfCats 元素调用发生了运行时错误。

TypeScript 只有一种泛型的简单 双变(bivariant) 实现,以供 JS 开发者采用。如果对变量命名得当,就能很大程度上避免指鸭为猫。

同时,存在向 TS 中增加 inout 约束的提案( https://github.com/microsoft/TypeScript/issues/10717),以支持协变和逆变。



相关文章
|
17天前
|
前端开发 JavaScript API
React Hooks 的使用场景有哪些?
【8月更文挑战第25天】
34 2
|
18天前
|
存储 前端开发 JavaScript
React Hooks的魔法:如何在组件世界里施展响应式与复用的魔法
【8月更文挑战第27天】React Hooks 是自 React 16.8 起新增的功能,支持开发者在无需类组件的情况下利用 React 的状态管理和特性。本文通过实例展示了多种核心 Hooks 的使用方法:`useState` 用于实现响应式状态管理;`useEffect` 处理副作用操作,如数据获取等;`useMemo` 和 `useCallback` 有助于性能优化;`useRef` 则提供对 DOM 的直接引用。
26 2
|
23天前
|
前端开发 JavaScript
react hooks深拷贝后无法保留视图状态
react hooks深拷贝后无法保留视图状态
|
14天前
|
容器 Kubernetes Docker
云原生JSF:在Kubernetes的星辰大海中,让JSF应用乘风破浪!
【8月更文挑战第31天】在本指南中,您将学会如何在Kubernetes上部署JavaServer Faces (JSF)应用,享受容器化带来的灵活性与可扩展性。文章详细介绍了从构建Docker镜像到配置Kubernetes部署全流程,涵盖Dockerfile编写、Kubernetes资源配置及应用验证。通过这些步骤,您的JSF应用将充分利用Kubernetes的优势,实现自动化管理和高效运行,开启Java Web开发的新篇章。
28 0
|
14天前
|
前端开发
【实战指南】React Hooks 详解超厉害!六个步骤带你提升 React 应用状态管理,快来探索!
【8月更文挑战第31天】React Hooks 是 React 16.8 推出的新特性,允许在函数组件中使用状态及其它功能而无需转换为类组件。通过以下六个步骤可有效提升 React 应用的状态管理:1)使用 `useState` Hook 添加状态;2)利用 `useEffect` Hook 执行副作用操作;3)在一个组件中结合多个 `useState` 管理不同状态;4)创建自定义 Hook 封装可重用逻辑;5)借助 `useContext` 访问上下文以简化数据传递;6)合理运用依赖项数组优化性能。React Hooks 为函数组件带来了更简洁的状态管理和副作用处理方式。
21 0
|
14天前
|
前端开发 JavaScript Android开发
React Native 快速入门简直太棒啦!构建跨平台移动应用的捷径,带你开启高效开发之旅!
【8月更文挑战第31天】React Native凭借其跨平台特性、丰富的生态系统及优异性能,成为移动应用开发的热门选择。它允许使用JavaScript和React语法编写一次代码即可在iOS和Android上运行,显著提升开发效率。此外,基于React框架的组件化开发模式使得代码更加易于维护与复用,加之活跃的社区支持与第三方库资源,加速了应用开发流程。尽管作为跨平台框架,React Native在性能上却不输原生应用,支持原生代码优化以实现高效渲染与功能定制。对于开发者而言,React Native简化了移动应用开发流程,是快速构建高质量应用的理想之选。
24 0
|
14天前
|
前端开发 JavaScript 数据管理
React Formik入门:简化表单处理的神器——全面掌握Formik在React表单开发中的高效应用与实战技巧
【8月更文挑战第31天】在React应用中,表单处理常常因繁琐而令人头疼。Formik作为一个开源库,专为简化React表单设计,减少冗余代码并提升处理效率。本文介绍Formik的使用方法及其优势,通过示例展示如何安装配置并创建基本表单,帮助开发者轻松应对各种表单需求。
22 0
|
14天前
|
前端开发 JavaScript 测试技术
React 与前端自动化测试也太重要啦!各种测试框架助力确保应用质量,快来开启优质开发之旅!
【8月更文挑战第31天】随着前端技术的发展,React 成为了构建用户界面的热门选择。然而,随着应用复杂性的增加,确保应用质量变得至关重要。本文介绍了前端自动化测试的重要性,并详细综述了常用的测试框架如 Jest、Enzyme 和 Cypress,以及如何通过它们进行高效的 React 组件测试。通过遵循最佳实践,如编写可维护的测试用例、覆盖关键场景、集成 CI/CD 流程和进行性能测试,可以显著提高应用的稳定性和可靠性。
25 0
|
21天前
|
前端开发
使用 React Hooks 的三个主要好处
【8月更文挑战第24天】
23 0
|
1月前
|
前端开发 JavaScript UED
React 基础与实践 | 青训营笔记
React 基础与实践 | 青训营笔记
39 0