最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React Hooks + TypeScript 最佳实践

    正文概述 掘金(Godiswill)   2021-01-18   959

    React Hooks + TypeScript 最佳实践

    原文

    本文根据日常开发实践,参考优秀文章、文档,来说说 TypeScript 是如何较优雅的融入 React 项目的。

    温馨提示:日常开发中已全面拥抱函数式组件和 React Hooksclass 类组件的写法这里不提及。

    前沿

    • 以前有 JSX 语法,必须引入 React。React 17.0+ 不需要强制声明 React 了。

    具体参考:介绍全新的 JSX 转换

    import React, { useState } from 'react';
    
    // 以后将被替代成
    import { useState } from 'react';
    import * as React from 'react';
    

    基础介绍

    基本类型

    • 基础类型就没什么好说的了,以下都是比较常用的,一般比较好理解,也没什么问题。
    type BasicTypes = {
        message: string;
        count: number;
        disabled: boolean;
        names: string[]; // or Array<string>
        id: string | number; // 联合类型
    }
    

    联合类型

    一般的联合类型,没什么好说的,这里提一下非常有用,但新手经常遗忘的写法 —— 字符字面量联合。

    • 例如:自定义 ajax 时,一般 method 就那么具体的几种:getpostput 等。

    大家都知道需要传入一个 string 型,你可能会这么写:

    type UnionsTypes = {
        method: string; // ❌ bad,可以传入任意字符串
    };
    
    • 使用字符字面量联合类型,第一、可以智能提示你可传入的字符常量;第二、防止拼写错误。后面会有更多的例子。
    type UnionsTypes = {
        method: 'get' | 'post'; // ✅ good 只允许 'get'、'post' 字面量
    };
    

    对象类型

    • 一般你知道确切的属性类型,这没什么好说的。
    type ObjectTypes = {
        obj3: {
            id: string;
            title: string;
        };
        objArr: {
            id: string;
            title: string;
        }[]; // 对象数组,or Array<{ id: string, title: string }>
    };
    
    • 但有时你只知道是个对象,而不确定具体有哪些属性时,你可能会这么用:
    type ObjectTypes = {
        obj: object; // ❌ bad,不推荐
        obj2: {}; // ❌ bad 几乎类似 object
    };
    
    • 一般编译器会提示你,不要这么使用,推荐使用 Record
    type ObjectTypes = {
        objBetter: Record<string, unknown>; // ✅ better,代替 obj: object
        
        // 对于 obj2: {}; 有三种情况:
        obj2Better1: Record<string, unknown>; // ✅ better 同上
        obj2Better2: unknown; // ✅ any value
        obj2Better3: Record<string, never>; // ✅ 空对象
        
        /** Record 更多用法 */
        dict1: {
            [key: string]: MyTypeHere;
        };
        dict2: Record<string, MyTypeHere>; // 等价于 dict1
    };
    
    • Record 有什么好处呢,先看看实现:
    // 意思就是,泛型 K 的集合作为返回对象的属性,且值类型为 T
    type Record<K extends keyof any, T> = {
        [P in K]: T;
    };
    
    • 官方的一个例子
    interface PageInfo {
        title: string;
    }
    
    type Page = 'home' | 'about' | 'contact';
    
    const nav: Record<Page, PageInfo> = {
        about: { title: 'about' },
        contact: { title: 'contact' },
        // TS2322: Type '{ about: { title: string; }; contact: { title: string; }; hoem: { title: string; }; }' 
        // is not assignable to type 'Record<Page, PageInfo>'. ...
        hoem: { title: 'home' },
    };
    
    nav.about;
    

    好处:

    1. 当你书写 home 值时,键入 h 常用的编辑器有智能补全提示;
    2. home 拼写错误成 hoem,会有错误提示,往往这类错误很隐蔽;
    3. 收窄接收的边界。

    函数类型

    • 函数类型不建议直接给 Function 类型,有明确的参数类型、个数与返回值类型最佳。
    type FunctionTypes = {
        onSomething: Function; // ❌ bad,不推荐。任何可调用的函数
        onClick: () => void; // ✅ better ,明确无参数无返回值的函数
        onChange: (id: number) => void; // ✅ better ,明确参数无返回值的函数
        onClick(event: React.MouseEvent<HTMLButtonElement>): void; // ✅ better
    };
    

    可选属性

    • React props 可选的情况下,比较常用。
    type OptionalTypes = {
        optional?: OptionalType; // 可选属性
    };
    
    • 例子:封装一个第三方组件,对方可能并没有暴露一个 props 类型定义时,而你只想关注自己的上层定义。 nameage 是你新增的属性,age 可选,other 为第三方的属性集。
    type AppProps = {
        name: string;
        age?: number;
        [propName: string]: any;
    };
    const YourComponent = ({ name, age, ...other }: AppProps) => (
        <div>
            {`Hello, my name is ${name}, ${age || 'unknown'}`}
            <Other {...other} />
        </div>
    );
    

    React Prop 类型

    • 如果你有配置 Eslint 等一些代码检查时,一般函数组件需要你定义返回的类型,或传入一些 React 相关的类型属性。

    这时了解一些 React 自定义暴露出的类型就很有必要了。例如常用的 React.ReactNode

    export declare interface AppProps {
        children1: JSX.Element; // ❌ bad, 没有考虑数组类型
        children2: JSX.Element | JSX.Element[]; // ❌ 没考虑字符类型
        children3: React.ReactChildren; // ❌ 名字唬人,工具类型,慎用
        children4: React.ReactChild[]; // better, 但没考虑 null
        children: React.ReactNode; // ✅ best, 最佳接收所有 children 类型
        functionChildren: (name: string) => React.ReactNode; // ✅ 返回 React 节点
        
        style?: React.CSSProperties; // React style
        
        onChange?: React.FormEventHandler<HTMLInputElement>; // 表单事件! 泛型参数即 `event.target` 的类型
    }
    

    更多参考资料

    函数式组件

    熟悉了基础的 TypeScript 使用 与 React 内置的一些类型后,我们该开始着手编写组件了。

    • 声明纯函数的最佳实践
    type AppProps = { message: string }; /* 也可用 interface */
    const App = ({ message }: AppProps) => <div>{message}</div>; // 无大括号的箭头函数,利用 TS 推断。
    
    • 需要隐式 children?可以试试 React.FC
    type AppProps = { title: string };
    const App: React.FC<AppProps> = ({ children, title }) => <div title={title}>{children}</div>;
    
    • 争议
    1. React.FC(or FunctionComponent)是显式返回的类型,而"普通函数"版本则是隐式的(有时还需要额外的声明)。
    2. React.FC 对于静态属性如 displayNamepropTypesdefaultProps 提供了自动补充和类型检查。
    3. React.FC 提供了默认的 children 属性的大而全的定义声明,可能并不是你需要的确定的小范围类型。
    4. 2和3都会导致一些问题。有人不推荐使用。

    目前 React.FC 在项目中使用较多。因为可以偷懒,还没碰到极端情况。

    Hooks

    项目基本上都是使用函数式组件和 React Hooks。 接下来介绍常用的用 TS 编写 Hooks 的方法。

    useState

    • 给定初始化值情况下可以直接使用
    import { useState } from 'react';
    // ...
    const [val, toggle] = useState(false);
    // val 被推断为 boolean 类型
    // toggle 只能处理 boolean 类型
    
    • 没有初始值(undefined)或初始 null
    type AppProps = { message: string };
    const App = () => {
        const [data] = useState<AppProps | null>(null);
        // const [data] = useState<AppProps | undefined>();
        return <div>{data && data.message}</div>;
    };
    
    • 更优雅,链式判断
    // data && data.message
    data?.message
    

    useEffect

    • 使用 useEffect 时传入的函数简写要小心,它接收一个无返回值函数或一个清除函数。
    function DelayedEffect(props: { timerMs: number }) {
        const { timerMs } = props;
    
        useEffect(
            () =>
                setTimeout(() => {
                    /* do stuff */
                }, timerMs),
            [timerMs]
        );
        // ❌ bad example! setTimeout 会返回一个记录定时器的 number 类型
        // 因为简写,箭头函数的主体没有用大括号括起来。
        return null;
    }
    
    • 看看 useEffect接收的第一个参数的类型定义。
    // 1. 是一个函数
    // 2. 无参数
    // 3. 无返回值 或 返回一个清理函数,该函数类型无参数、无返回值 。
    type EffectCallback = () => (void | (() => void | undefined));
    
    • 了解了定义后,只需注意加层大括号。
    function DelayedEffect(props: { timerMs: number }) {
        const { timerMs } = props;
    
        useEffect(() => {
            const timer = setTimeout(() => {
                /* do stuff */
            }, timerMs);
            
            // 可选
            return () => clearTimeout(timer);
        }, [timerMs]);
        // ✅ 确保函数返回 void 或一个返回 void|undefined 的清理函数
        return null;
    }
    
    • 同理,async 处理异步请求,类似传入一个 () => Promise<void>EffectCallback 不匹配。
    // ❌ bad
    useEffect(async () => {
        const { data } = await ajax(params);
        // todo
    }, [params]);
    
    • 异步请求,处理方式:
    // ✅ better
    useEffect(() => {
        (async () => {
            const { data } = await ajax(params);
            // todo
        })();
    }, [params]);
    
    // 或者 then 也是可以的
    useEffect(() => {
        ajax(params).then(({ data }) => {
            // todo
        });
    }, [params]);
    

    useRef

    useRef 一般用于两种场景

    1. 引用 DOM 元素;
    2. 不想作为其他 hooks 的依赖项,因为 ref 的值引用是不会变的,变的只是 ref.current
    • 使用 useRef ,可能会有两种方式。
    const ref1 = useRef<HTMLElement>(null!);
    const ref2 = useRef<HTMLElement | null>(null);
    
    • 非 null 断言 null!。断言之后的表达式非 null、undefined
    function MyComponent() {
        const ref1 = useRef<HTMLElement>(null!);
        useEffect(() => {
            doSomethingWith(ref1.current);
            // 跳过 TS null 检查。e.g. ref1 && ref1.current
        });
        return <div ref={ref1}> etc </div>;
    }
    
    • 不建议使用 !,存在隐患,Eslint 默认禁掉。
    function TextInputWithFocusButton() {
        // 初始化为 null, 但告知 TS 是希望 HTMLInputElement 类型
        // inputEl 只能用于 input elements
        const inputEl = React.useRef<HTMLInputElement>(null);
        const onButtonClick = () => {
            // TS 会检查 inputEl 类型,初始化 null 是没有 current 上是没有 focus 属性的
            // 你需要自定义判断! 
            if (inputEl && inputEl.current) {
                inputEl.current.focus();
            }
            // ✅ best
            inputEl.current?.focus();
        };
        return (
            <>
                <input ref={inputEl} type="text" />
                <button onClick={onButtonClick}>Focus the input</button>
            </>
        );
    }
    

    useReducer

    使用 useReducer 时,多多利用 Discriminated Unions 来精确辨识、收窄确定的 typepayload 类型。 一般也需要定义 reducer 的返回类型,不然 TS 会自动推导。

    • 又是一个联合类型收窄和避免拼写错误的精妙例子。
    const initialState = { count: 0 };
    
    // ❌ bad,可能传入未定义的 type 类型,或码错单词,而且还需要针对不同的 type 来兼容 payload
    // type ACTIONTYPE = { type: string; payload?: number | string };
    
    // ✅ good
    type ACTIONTYPE =
        | { type: 'increment'; payload: number }
        | { type: 'decrement'; payload: string }
        | { type: 'initial' };
    
    function reducer(state: typeof initialState, action: ACTIONTYPE) {
        switch (action.type) {
            case 'increment':
                return { count: state.count + action.payload };
            case 'decrement':
                return { count: state.count - Number(action.payload) };
            case 'initial':
                return { count: initialState.count };
            default:
                throw new Error();
        }
    }
    
    function Counter() {
        const [state, dispatch] = useReducer(reducer, initialState);
        return (
            <>
                Count: {state.count}
                <button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>-</button>
                <button onClick={() => dispatch({ type: 'increment', payload: 5 })}>+</button>
            </>
        );
    }
    

    useContext

    一般 useContextuseReducer 结合使用,来管理全局的数据流。

    • 例子
    interface AppContextInterface {
        state: typeof initialState;
        dispatch: React.Dispatch<ACTIONTYPE>;
    }
    
    const AppCtx = React.createContext<AppContextInterface>({
        state: initialState,
        dispatch: (action) => action,
    });
    const App = (): React.ReactNode => {
        const [state, dispatch] = useReducer(reducer, initialState);
    
        return (
            <AppCtx.Provider value={{ state, dispatch }}>
                <Counter />
            </AppCtx.Provider>
        );
    };
    
    // 消费 context
    function Counter() {
        const { state, dispatch } = React.useContext(AppCtx);
        return (
            <>
                Count: {state.count}
                <button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>-</button>
                <button onClick={() => dispatch({ type: 'increment', payload: 5 })}>+</button>
            </>
        );
    }
    

    自定义 Hooks

    Hooks 的美妙之处不只有减小代码行的功效,重点在于能够做到逻辑与 UI 分离。做纯粹的逻辑层复用。

    • 例子:当你自定义 Hooks 时,返回的数组中的元素是确定的类型,而不是联合类型。可以使用 const-assertions 。
    export function useLoading() {
        const [isLoading, setState] = React.useState(false);
        const load = (aPromise: Promise<any>) => {
            setState(true);
            return aPromise.finally(() => setState(false));
        };
        return [isLoading, load] as const; // 推断出 [boolean, typeof load],而不是联合类型 (boolean | typeof load)[]
    }
    
    • 也可以断言成 tuple type 元组类型。
    export function useLoading() {
        const [isLoading, setState] = React.useState(false);
        const load = (aPromise: Promise<any>) => {
            setState(true);
            return aPromise.finally(() => setState(false));
        };
        return [isLoading, load] as [
            boolean, 
            (aPromise: Promise<any>) => Promise<any>
        ];
    }
    
    • 如果对这种需求比较多,每个都写一遍比较麻烦,可以利用泛型定义一个辅助函数,且利用 TS 自动推断能力。
    function tuplify<T extends any[]>(...elements: T) {
        return elements;
    }
    
    function useArray() {
        const numberValue = useRef(3).current;
        const functionValue = useRef(() => {}).current;
        return [numberValue, functionValue]; // type is (number | (() => void))[]
    }
    
    function useTuple() {
        const numberValue = useRef(3).current;
        const functionValue = useRef(() => {
        }).current;
        return tuplify(numberValue, functionValue); // type is [number, () => void]
    }
    

    扩展

    工具类型

    学习 TS 好的途径是查看优秀的文档和直接看 TS 或类库内置的类型。这里简单做些介绍。

    • 如果你想知道某个函数返回值的类型,你可以这么做
    // foo 函数原作者并没有考虑会有人需要返回值类型的需求,利用了 TS 的隐式推断。
    // 没有显式声明返回值类型,并 export,外部无法复用
    function foo(bar: string) {
        return { baz: 1 };
    }
    
    // TS 提供了 ReturnType 工具类型,可以把推断的类型吐出
    type FooReturn = ReturnType<typeof foo>; // { baz: number }
    
    • 类型可以索引返回子属性类型
    function foo() {
        return {
            a: 1,
            b: 2,
            subInstArr: [
                {
                    c: 3,
                    d: 4,
                },
            ],
        };
    }
    
    type InstType = ReturnType<typeof foo>;
    type SubInstArr = InstType['subInstArr'];
    type SubIsntType = SubInstArr[0];
    
    const baz: SubIsntType = {
        c: 5,
        d: 6, // type checks ok!
    };
    
    // 也可一步到位
    type SubIsntType2 = ReturnType<typeof foo>['subInstArr'][0];
    const baz2: SubIsntType2 = {
        c: 5,
        d: 6, // type checks ok!
    };
    

    同理工具类型 Parameters 也能推断出函数参数的类型。

    • 简单的看看实现:关键字 infer
    type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
    type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
    

    T extends (...args: any) => infer R ? R : any; 的意思是 T 能够赋值给 (...args: any) => any 的话,就返回该函数推断出的返回值类型 R

    defaultProps

    默认值问题。

    type GreetProps = { age: number } & typeof defaultProps;
    const defaultProps = {
        age: 21,
    };
    
    const Greet = (props: GreetProps) => {
        // etc
    };
    Greet.defaultProps = defaultProps;
    
    • 你可能不需要 defaultProps
    type GreetProps = { age?: number };
    
    const Greet = ({ age = 21 }: GreetProps) => { 
        // etc 
    };
    

    消除魔术数字/字符

    本人比较痛恨的一些代码点。

    • 糟糕的例子,看到下面这段代码不知道你的内心,有没有羊驼奔腾。
    if (status === 0) {
        // ...
    } else {
        // ...
    }
    
    // ...
    
    if (status === 1) {
        // ...
    }
    
    • 利用枚举,统一注释且语义化
    // enum.ts
    export enum StatusEnum {
        Doing,   // 进行中
        Success, // 成功
        Fail,    // 失败
    }
    
    //index.tsx
    if (status === StatusEnum.Doing) {
        // ...
    } else {
        // ...
    }
    
    // ...
    
    if (status === StatusEnum.Success) {
        // ...
    }
    
    • ts enum 略有争议,有的人推崇去掉 ts 代码依旧能正常运行,显然 enum 不行。
    // 对象常量
    export const StatusEnum = {
        Doing: 0,   // 进行中
        Success: 1, // 成功
        Fail: 2,    // 失败
    };
    
    • 如果字符单词本身就具有语义,你也可以用字符字面量联合类型来避免拼写错误
    export declare type Position = 'left' | 'right' | 'top' | 'bottom';
    let position: Position;
    
    // ...
    
    // TS2367: This condition will always return 'false' since the types 'Position' and '"lfet"' have no overlap.
    if (position === 'lfet') { // 单词拼写错误,往往这类错误比较难发现
        // ...
    }
    

    延伸:策略模式消除 if、else

    if (status === StatusEnum.Doing) {
        return '进行中';
    } else if (status === StatusEnum.Success) {
        return '成功';
    } else {
        return '失败';
    }
    
    • 策略模式
    // 对象常量
    export const StatusEnumText = {
        [StatusEnum.Doing]: '进行中',
        [StatusEnum.Success]: '成功',
        [StatusEnum.Fail]: '失败',
    };
    
    // ...
    return StatusEnumText[status];
    

    参考资料

    1. React+TypeScript Cheatsheets
    2. The TypeScript Handbook
    3. Typescript 中文文档

    起源地下载网 » React Hooks + TypeScript 最佳实践

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元