likes
comments
collection
share

React+TypeScript积累实践

作者站长头像
站长
· 阅读数 13

前言

在近些年,React已经成为了开发Web应用的首选框架之一,而TypeScript则是JavaScript的一个超集,在大型项目中能够带来更好的类型安全和代码可维护性。将React和TypeScript结合使用,可以更加高效地开发出可靠且易于维护的应用程序。

本篇文章将介绍我在项目实践中如何使用React和TypeScript,包括设置项目根目录下配置文件、函数式组件编写、Hooks实践、事件处理、处理接口返回数据、状态管理等方面的内容,希望这篇文章能够为你提供一些参考和启发。

准备知识

参考TypeScript playground React 部分

参考 React 官方文档,TS部分

本文档参考 TypeScript 最新版本

配置文件

tsconfig.json 配置文件:

{
  "compilerOptions": {

   /* 基本选项 */
  "target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
  "module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
  "lib": [], // 指定要包含在编译中的库文件
  "allowJs": true, // 允许编译 javascript 文件
  "checkJs": true, // 报告 javascript 文件中的错误
  "jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
  "declaration": true, // 生成相应的 '.d.ts' 文件
  "sourceMap": true, // 生成相应的 '.map' 文件
  "outFile": "./", // 将输出文件合并为一个文件
  "outDir": "./", // 指定输出目录
  "rootDir": "./", // 用来控制输出目录结构 --outDir.
  "removeComments": true, // 删除编译后的所有的注释
  "noEmit": true, // 不生成输出文件
  "importHelpers": true, // 从 tslib 导入辅助工具函数
  "isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ],
    
  /* 严格的类型检查选项 */
  "strict": true, // 启用所有严格类型检查选项
  "noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
  "strictNullChecks": true, // 启用严格的 null 检查
  "noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
  "alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

  /* 额外的检查 */
  "noUnusedLocals": true, // 有未使用的变量时,抛出错误
  "noUnusedParameters": true, // 有未使用的参数时,抛出错误
  "noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
  "noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许switch 的 case 语句贯穿)
    
 /* 模块解析选项 */
 "moduleResolution": "node", // // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
 "baseUrl": "./", // 用于解析非相对模块名称的基目录
 "paths": {
    "@/assets/*": ["assets/*"],
    "@/components/*": ["components/*"],
    "@/config/*": ["config/*"],
    "@/models/*": ["models/*"],
    "@/navigation/*": ["navigation/*"],
    "@/views/*": ["views/*"],
    "@/utils/*": ["utils/*"],
    "@/constants/*": ["constants/*"],
    "@/hooks/*": ["hooks/*"],
    "@/screens/*": ["screens/*"],
    "@/service/*": ["service/*"]
 },
 "rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
 "typeRoots": [], // 包含类型声明的文件列表
 "types": [], // 需要包含的类型声明文件名列表
 "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。

 /* Source Map Options */
 "sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
 "mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
 "inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
 "inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

 /* 其他选项 */
 "experimentalDecorators": true, // 启用装饰器
 "emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}    

babel.config.js 配置文件:

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      ['import', { libraryName: '@ant-design/react-native' }],
      [
        'module-resolver',
        {
          root: ['./'],
          alias: {
            '@/components': './components',
            '@/utils': './utils',
            '@/views': './views',
            '@/navigation': './navigation',
            '@/models': './models',
            '@/config': './config',
            '@/constants': './constants',
            '@/assets': './assets',
            '@/screens': './screens',
            '@/hooks': './hooks',
            '@/service': './service'
          }
        }
      ]
    ]
  };
};

函数式组件编写

使用React.FC(推荐)

使用 React.FC 的方式声明最简单有效:

  • React.FC 显式地定义了返回类型,其他方式是隐式推导;
  • React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全;
  • React.FC 为 children 提供了隐式的类型(ReactElement | null);
// Great
type AppProps = {
  message: string
}
const App: React.FC<AppProps> = ({ message, children }) => (
  <div>
    {message}
    {children}
  </div>
)

使用PropsWithChildren

这种方式可以省去频繁定义 children 的类型,自动设置 children 类型为 ReactNode:

type AppProps = React.PropsWithChildren<{ message: string }>
const App = ({ message, children }: AppProps) => (
  <div>
    {message}
    {children}
  </div>
)

直接声明

type AppProps = {
  message: string
  children?: React.ReactNode
}
const App = ({ message, children }: AppProps) => (
  <div>
    {message}
    {children}
  </div>
)

Hooks实践

useState<T>

大部分情况下,TS 会自动为你推导 state 的类型:

// `val`会推导为boolean类型, toggle接收boolean类型参数
const [val, toggle] = React.useState(false)
// obj会自动推导为类型: {name: string}
const [obj] = React.useState({ name: 'sj' })
// arr会自动推导为类型: string[]
const [arr] = React.useState(['One', 'Two'])

使用推导类型作为接口/类型:

export default function App() {
  // user会自动推导为类型: {name: string}
  const [user] = React.useState({ name: 'sj', age: 32 })
  const showUser = React.useCallback((obj: typeof user) => {
  return `My name is ${obj.name}, My age is ${obj.age}`
  }, [])
  return <div className="App">用户: {showUser(user)}</div>
}

但是,一些状态初始值为空时(null),需要显示地声明类型:

type User = {
  name: string
  age: number
}
const [user, setUser] = React.useState<User | null>(null)

useRef<T>

  1. 存储 DOM 节点的引用:

    function MyComponent() {
      // 当初始值为null时,这种方式的inputRef.current 是只读的(read-only),并且可以传递给内置的 ref 属性,绑定 DOM 元素 ;
      const inputRef = useRef<HTMLInputElement>(null);
      // 这种方式的inputRef.current 是可变的(类似于声明类的成员变量)
      // const inputRef = useRef<HTMLInputElement | null>(null);
      function focusInput() {
        // 都需要对类型进行检查,用?
        inputRef.current?.focus();
      }
      return (
        <div>
          <input type="text" ref={inputRef} />
          <button onClick={focusInput}>Focus Input</button>
        </div>
      );
    }
    
  2. 存储任意值的引用,react的state变化不会引起这个引用的值改变,可以缓存函数内部的变量,类似闭包特性:

    function MyComponent() {
      const counterRef = useRef<number>(0);
      function incrementCounter() {
        counterRef.current += 1;
        console.log(`Counter: ${counterRef.current}`);
      }
      return (
        <div>
          <button onClick={incrementCounter}>Increment Counter</button>
        </div>
      );
    }
    
  3. 保存上一次的 props 或 state:

    function MyComponent({ value }: { value: number }) {
      const previousValueRef = useRef<number | null>(null);
      useEffect(() => {
        previousValueRef.current = value;
      });
        const message = previousValueRef.current !== null
        ? `Previous value was ${previousValueRef.current}`
        : 'No previous value';
      return <div>{message}</div>;
    }
    

总结:useRef<T> 返回的 ref 对象在组件重新渲染时是不会变化的,因此可以用来存储一些需要在组件生命周期内保持稳定的值。如果需要在每次组件渲染时重新计算一个值,应该使用 useState 或类似的 Hook。

useEffect

useEffect 需要注意回调函数的返回值只能是函数或者 undefined

// undefined作为回调函数的返回值
React.useEffect(() => {
// do something...
}, [])
// 返回值是一个函数
React.useEffect(() => {
// do something...
 return () => {}
}, [])

useMemo<T>

useMemo 是 React 的一个 Hook,用于对计算昂贵的值进行缓存,可以避免在组件重新渲染时重复计算。它接收一个回调函数和一个依赖项数组作为参数,并返回回调函数的结果。在 TypeScript 中,你需要指定泛型类型 T,来标识回调函数的返回类型。例如:

// 显式的指定返回值类型,返回值不一致会报错
const memoizedValue = useMemo<number>(() => {
  // ... 需要进行缓存的计算
  console.log('calculating sum');
  return dependencies.reduce((acc, value) => acc + value, 0);
}, [dependencies]);

useCallback<T>

useCallback 是 React 中的一个 hook,它可以用于优化渲染性能。useCallback 接受两个参数:第一个参数是一个回调函数,第二个参数是一个依赖项数组。当依赖项数组中的任何一个值发生变化时,useCallback 将返回一个新的回调函数。如果依赖项数组没有发生变化,则 useCallback 返回缓存的旧回调函数。通常情况下,useCallback 会在需要将回调函数传递给子组件时使用。这样做可以避免每次渲染时都创建一个新的回调函数。

function MyComponent() {
    const handleChange = useCallback<
      React.ChangeEventHandler<HTMLInputElement>
    >(evt => {
      // 表单事件
      console.log(evt.target.value)
    }, [])
    return (
        <input onChange={handleChange}>
          {/* 表单内容 */}
        </input>
    );
}
// 在上面的示例代码中,当 MyComponent 组件重新渲染时,由于依赖项数组为空,handleChange 回调函数不会被重新创建。这样做可以提高性能,避免不必要的回调函数创建和内存分配。

注意:useMemouseCallback 都可以直接从它们返回的值中推断出它们的类型。

自定义Hook

自定义 Hook 的返回值如果是数组类型,TS 会自动推导为 Union 类型,而我们实际需要的是数组里每一项的具体类型,需要手动添加 const 断言 进行处理:

function useLoading() {
  const [isLoading, setState] = React.useState(false)
  const load = (aPromise: Promise<any>) => {
    setState(true)
    return aPromise.then(() => setState(false))
  }
  // 实际需要: [boolean, typeof load] 类型
  // 而不是自动推导的:(boolean | typeof load)[]
  return [isLoading, load] as const

}

如果使用 const 断言遇到问题,也可以直接定义返回类型:

export function useLoading(): [
  boolean,
  (aPromise: Promise<any>) => Promise<any>
] {
const [isLoading, setState] = React.useState(false)
  const load = (aPromise: Promise<any>) => {
    setState(true)
    return aPromise.then(() => setState(false))
  }
  return [isLoading, load]
}

如果有大量的自定义 Hook 需要处理,这里有一个方便的工具方法可以处理 tuple 返回值:

function tuplify<T extends any[]>(...elements: T) {
  return elements
}
function useLoading() {
  const [isLoading, setState] = React.useState(false)
  const load = (aPromise: Promise<any>) => {
    setState(true)
    return aPromise.then(() => setState(false))
  }
  // (boolean | typeof load)[]
  return [isLoading, load]
}
function useTupleLoading() {
  const [isLoading, setState] = React.useState(false)
  const load = (aPromise: Promise<any>) => {
    setState(true)
    return aPromise.then(() => setState(false))
  }
  // [boolean, typeof load]
  return tuplify(isLoading, load)
}

获取字典数据Hook

页面获取的字典数据一般不会比较固定,提高代码复用性:

function useDictionaryData(dictionaryName) {
  const [dictionaryData, setDictionaryData] = useState(null);
  useEffect(() => {
    async function fetchData() {
      try {
        // 这里的字典数据也可以考虑从全局store里面取
        const response = await fetch(`/api/...`);
        const data = await response.json();
        setDictionaryData(data);
        } catch (error) {
        console.error(error);
      }
    }
  fetchData();
  }, [dictionaryName]);
  return dictionaryData;
}
// 使用:
const dictionaryData = useDictionaryData(dictionaryName);

用Type还是Interface

它们俩都可以用来描述对象,interface 更适合用来描述对象(object)的形状,type可以表示更复杂的类型,与interface不同的是,type 可以使用 typeof 操作符创建类型别名。先说区别:

区别

  1. type 可以声明基本类型别名、联合类型、元祖等类型。

    type Name = string
    interface Dog {
      wong();
    }
    interface Cat {
      miao();
    }
    type Pet = Dog | Cat;
    let a: Pet = {
      wong() {},
    };
    const div = document.createElement('div');
    type A = typeof div;
    // type类型不支持属性扩展
    // Error: Duplicate identifier 'Cat'
    type Cat {
      sleep();
    }
    
  2. interface 可以声明合并,可以使用extends实现继承,type不允许使用extends和implement,type可以使用&来实现继承

    interface App {
      name: string
    }
    interface App {
      type: 'mobile' | 'web' | 'desktop'
    }
    // TypeScript将自动合并
    const app: App = {
      name: 'facebook',
      type: 'mobile'
    }
    interface Apps extends App {
        title: string
    }
    

使用

  • 在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口

    export interface IHttpResponse {
      data: any
      code: number
      message: string
      success: boolean
    }
    
  • 在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type的约束性更强

    type RNPickerProps {
      name: string;
      label: string;
      onChange?(val: CascaderValue | undefined): void;
      data: PickerData[] | PickerData[][];
      errors?: any;
      required?: boolean;
    }
    
  • 获取未导出的 Type。某些场景下我们在引入第三方的库时会发现想要使用的组件并没有导出我们需要的组件参数类型或者返回值类型,这时候我们可以通过 ComponentProps/ ReturnType 来获取到想要的类型。

    // 获取参数类型
    import { Button } from 'library' // 但是未导出props type
    type ButtonProps = React.ComponentProps<typeof Button> // 获取props
    type AlertButtonProps = Omit<ButtonProps, 'onClick'> // 去除onClick
    const AlertButton: React.FC<AlertButtonProps> = props => (
      <Button onClick={() => alert('hello')} {...props} />
    )
    // 获取返回值类型
    function foo() {
      return { baz: 1 }
    }
    type FooReturn = ReturnType<typeof foo> // { baz: number }
    

常用 React 属性类型

export declare interface AppBetterProps {
  children: React.ReactNode // 一般情况下推荐使用,支持所有类型
  functionChildren: (name: string) => React.ReactNode
  style?: React.CSSProperties // 传递style对象
  onChange?: React.FormEventHandler<HTMLInputElement> // 表单事件, 泛型参数是event.target的类型
}

事件处理

onChange事件

  1. 第一种方法使用推断的方法签名(例如:React.FormEvent :void):

    type changeFn = (e: React.FormEvent<HTMLInputElement>) => void
    const App: React.FC = () => {
      const [state, setState] = React.useState('')
      const onChange: changeFn = e => {
        setState(e.currentTarget.value)
      }
      return (
        <div>
          <input type="text" value={state} onChange={onChange} />
        </div>
      )
    }
    
  2. 第二种方法强制使用 @types / react 提供的委托类型:

    const App: React.FC = () => {
      const [state, setState] = React.useState('')
      const onChange: React.ChangeEventHandler<HTMLInputElement> = e => {
        setState(e.currentTarget.value)
      }
      return (
        <div>
          <input type="text" value={state} onChange={onChange} />
        </div>
      )
    }
    

onSubmit事件

如果不太关心事件的类型,可以直接使用 React.SyntheticEvent,如果目标表单有想要访问的自定义命名输入,可以使用类型扩展:

const App: React.FC = () => {
  const onSubmit = (e: React.SyntheticEvent) => {
    e.preventDefault()
    const target = e.target as typeof e.target & {
      password: { value: string }
    } // 类型扩展
    const password = target.password.value
  }
  return (
    <form onSubmit={onSubmit}>
      <div>
        <label>
          Password:
          <input type="password" name="password" />
        </label>
      </div>
      <div>
        <input type="submit" value="Log in" />
      </div>
    </form>
  )
}

event事件

我们在进行事件注册时经常会在事件处理函数中使用 event 事件对象,例如当使用鼠标事件时我们通过 clientXclientY 去获取指针的坐标。你可能会想到直接把 event 设置为 any 类型,但是这样就失去了我们对代码进行静态检查的意义。

试想下当我们注册一个 Touch 事件,然后错误的通过事件处理函数中的 event 对象去获取其 clientY 属性的值,在这里我们已经将 event 设置为 any 类型,导致 TypeScript 在编译时并不会提示我们错误, 当我们通过 event.clientY 访问时就有问题了,因为 Touch 事件的 event 对象并没有 clientY 这个属性。

通过 interfaceevent 对象进行类型声明编写的话又十分浪费时间,幸运的是 React 的声明文件提供了 Event 对象的类型声明。

  • ClipboardEvent<T = Element> 剪切板事件对象
  • DragEvent<T =Element> 拖拽事件对象
  • ChangeEvent<T = Element> Change 事件对象
  • KeyboardEvent<T = Element> 键盘事件对象
  • MouseEvent<T = Element> 鼠标事件对象
  • TouchEvent<T = Element> 触摸事件对象
  • WheelEvent<T = Element> 滚轮时间对象
  • AnimationEvent<T = Element> 动画事件对象
  • TransitionEvent<T = Element> 过渡事件对象

当我们定义事件处理函数时有没有更方便定义其函数类型的方式呢?答案是使用 React 声明文件所提供的 EventHandler 类型别名,通过不同事件的 EventHandler 的类型别名来定义事件处理函数的类型:

type EventHandler<E extends React.SyntheticEvent<any>> = {
  bivarianceHack(event: E): void
}['bivarianceHack']
type ReactEventHandler<T = Element> = EventHandler<React.SyntheticEvent<T>>
// 剪切板事件
type ClipboardEventHandler<T = Element> = EventHandler<React.ClipboardEvent<T>>
// 拖拽事件
type DragEventHandler<T = Element> = EventHandler<React.DragEvent<T>>
// 鼠标聚焦事件
type FocusEventHandler<T = Element> = EventHandler<React.FocusEvent<T>>
// 表单事件
type FormEventHandler<T = Element> = EventHandler<React.FormEvent<T>>
// 值改变事件
type ChangeEventHandler<T = Element> = EventHandler<React.ChangeEvent<T>>
// 键盘事件
type KeyboardEventHandler<T = Element> = EventHandler<React.KeyboardEvent<T>>
// 鼠标事件
type MouseEventHandler<T = Element> = EventHandler<React.MouseEvent<T>>
// 触摸事件
type TouchEventHandler<T = Element> = EventHandler<React.TouchEvent<T>>
// 指针事件
type PointerEventHandler<T = Element> = EventHandler<React.PointerEvent<T>>
// UI事件
type UIEventHandler<T = Element> = EventHandler<React.UIEvent<T>>
// 滚轮时间对象
type WheelEventHandler<T = Element> = EventHandler<React.WheelEvent<T>>
// 动画事件
type AnimationEventHandler<T = Element> = EventHandler<React.AnimationEvent<T>>
// 过渡动画事件
type TransitionEventHandler<T = Element> = EventHandler<
React.TransitionEvent<T>

bivarianceHack 为事件处理函数的类型定义,函数接收一个 event 对象,并且其类型为接收到的泛型变量 E 的类型, 返回值为 void

操作符

  • typeof and instanceof: 用于类型区分
  • keyof: 获取 object 的 key
  • O[K]: 属性查找
  • [K in O]: 映射类型
  • + or - or readonly or ?: 加法、减法、只读和可选修饰符
  • x ? Y : Z: 用于泛型类型、类型别名、函数参数类型的条件类型
  • !: 可空类型的空断言
  • as: 类型断言
  • is: 函数返回类型的类型保护

使用查找类型访问组件属性类型

通过查找类型减少 type 的非必要导出,如果需要提供复杂的 type,应当提取到作为公共 API 导出的文件中。

现在我们有一个 Counter 组件,需要 name 这个必传参数:

export type Props = {
  name: string
}
const Counter: React.FC<Props> = props => {
  return <></>
}
export default Counter

在其他引用它的组件中我们有两种方式获取到 Counter 的参数类型:

// Great
import Counter from './components
type PropsNew = React.ComponentProps<typeof Counter> & {
  age: number
}
const App: React.FC<PropsNew> = props => {
  return <Counter {...props} />
}

第二种是通过在原组件进行导出

import Counter, { Props } from './components'
type PropsNew = Props & {
  age: number
}
const App: React.FC<PropsNew> = props => {
  return (
    <Counter {...props} />
  )
}

Promise

在做异步操作时我们经常使用 async 函数,函数调用时会 return 一个 Promise 对象,可以使用 then 方法添加回调函数。Promise 是一个泛型类型,T 泛型变量用于确定 then 方法时接收的第一个回调函数的参数类型:

type IResponse<T> = {
  message: string
  result: T
  success: boolean
}
async function getResponse(): Promise<IResponse<number[]>> {
  return {
    message: '获取成功',
    result: [1, 2, 3],
    success: true,
  }
}
getResponse().then(response => {
  console.log(response.result)
})

处理接口返回数据

TypeScript 对接口返回数据的处理,通常会写返回数据的interface获得丰富的代码提示以及语法检测能力,但这个过程需要花费时间编写,我们该如何高效的解决这个问题?

  • 利用工具,JsonToAny,能够轻松的将接口返回的 JSON 数据转换成我们前端所需要的 interface

最大限度的节省了我们手动定义 interface 的时间。

React+TypeScript积累实践

  • 其他工具,pont,使用方式点击见github

状态管理

在React应用程序中,Zustand是一个轻量级、快速和灵活的状态管理库。

interface IStoreState {
  USER_INFO: IUserInfo | null
  fetchLogin: (params: loginFormValues, cb: Function) => void
}
const useStore = create<IStoreState>()(
  devtools(
    // store数据持久化
    persist(
      (set, get) => ({
        USER_INFO: null,
        fetchLogin: async (payload: loginFormValues, cb) => {
          const res = await fetchLogin(payload)
          const info = await getBaseUserInfo()
          set({
            USER_INFO: {
              token: res.data,
              ...info.data
            }
          })
          cb()
        },
        }),
        {
        name: 'STORAGE_DATA', // unique name
        storage: createJSONStorage(() => AsyncStorage)
      }
    )
  )
)
// 页面组件使用
const { USER_INFO,fetchLogin } = useStore()