这篇文章主要讲解了“怎么在React中完美运用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“怎么在React中完美运用”吧!
一直以来,ssh 身边都有很多小伙伴对 TS 如何在 React 中运用有很多困惑,他们开始慢慢讨厌 TS,觉得各种莫名其妙的问题降低了开发的效率。
其实如果运用熟练的话,TS 只是在第一次开发的时候稍微多花一些时间去编写类型,后续维护、重构的时候就会发挥它神奇的作用了,还是非常推荐长期维护的项目使用它的。
其实我一直知道英文版有个不错的备忘录,本来想直接推荐给小伙伴,奈何很多人对英文比较头痛,而它中文翻译的版本点进去竟然是这个景象:
既然如此,就自己动手。结合英文原版里的一些示例进行一些扩展,总结成这篇备忘录。
阅读本文的前提条件是:
熟悉 React 的使用。
熟悉 TypeScript 中的类型知识。
本文会侧重使用 React Hook 作为示例,当然大部分类型知识都是通用的。
也就是说,这篇文章侧重点在于 「React 和 TypeScript 的结合」,而不是基础知识,基础知识阅读文档即可学习。
TypeScript Playground with React:可以在线调试 React + TypeScript,只能调试类型,并不能运行代码
Stackblitz:云开发工具,可以直接运行 React 代码并且预览
Create React App TypeScript: 本地用脚手架生成 React + TS 的项目
选择你觉得比较中意的调试工具即可。
先看几种定义 Props 经常用到的类型:
基础类型
type BasicProps = { message: string; count: number; disabled: boolean; /** 数组类型 */ names: string[]; /** 用「联合类型」限制为下面两种「字符串字面量」类型 */ status: "waiting" | "success"; };
对象类型
type ObjectOrArrayProps = { /** 如果你不需要用到具体的属性 可以这样模糊规定是个对象 ❌ 不推荐 */ obj: object; obj2: {}; // 同上 /** 拥有具体属性的对象类型 ✅ 推荐 */ obj3: { id: string; title: string; }; /** 对象数组 ? 常用 */ objArr: { id: string; title: string; }[]; /** key 可以为任意 string,值限制为 MyTypeHere 类型 */ dict1: { [key: string]: MyTypeHere; }; dict2: Record<string, MyTypeHere>; // 基本上和 dict1 相同,用了 TS 内置的 Record 类型。 }
函数类型
type FunctionProps = { /** 任意的函数类型 ❌ 不推荐 不能规定参数以及返回值类型 */ onSomething: Function; /** 没有参数的函数 不需要返回值 ? 常用 */ onClick: () => void; /** 带函数的参数 ? 非常常用 */ onChange: (id: number) => void; /** 另一种函数语法 参数是 React 的按钮事件 ? 非常常用 */ onClick(event: React.MouseEvent<HTMLButtonElement>): void; /** 可选参数类型 ? 非常常用 */ optional?: OptionalType; }
React 相关类型
export declare interface AppProps { children1: JSX.Element; // ❌ 不推荐 没有考虑数组 children2: JSX.Element | JSX.Element[]; // ❌ 不推荐 没有考虑字符串 children children4: React.ReactChild[]; // 稍微好点 但是没考虑 null children: React.ReactNode; // ✅ 包含所有 children 情况 functionChildren: (name: string) => React.ReactNode; // ✅ 返回 React 节点的函数 style?: React.CSSProperties; // ✅ 推荐 在内联 style 时使用 // ✅ 推荐原生 button 标签自带的所有 props 类型 // 也可以在泛型的位置传入组件 提取组件的 Props 类型 props: React.ComponentProps<"button">; // ✅ 推荐 利用上一步的做法 再进一步的提取出原生的 onClick 函数类型 // 此时函数的第一个参数会自动推断为 React 的点击事件类型 onClickButton:React.ComponentProps<"button">["onClick"] }
最简单的:
interface AppProps = { message: string }; const App = ({ message }: AppProps) => <div>{message}</div>;
包含 children 的:
利用 React.FC 内置类型的话,不光会包含你定义的 AppProps 还会自动加上一个 children 类型,以及其他组件上会出现的类型:
// 等同于 AppProps & { children: React.ReactNode propTypes?: WeakValidationMap<P>; contextTypes?: ValidationMap<any>; defaultProps?: Partial<P>; displayName?: string; } // 使用 interface AppProps = { message: string }; const App: React.FC<AppProps> = ({ message, children }) => { return ( <> {children} <div>{message}</div> </> ) };
@types/react 包在 16.8 以上的版本开始对 Hooks 的支持。
useState
如果你的默认值已经可以说明类型,那么不用手动声明类型,交给 TS 自动推断即可:
// val: boolean const [val, toggle] = React.useState(false); toggle(false) toggle(true)
如果初始值是 null 或 undefined,那就要通过泛型手动传入你期望的类型。
const [user, setUser] = React.useState<IUser | null>(null); // later... setUser(newUser);
这样也可以保证在你直接访问 user 上的属性时,提示你它有可能是 null。
通过 optional-chaining 语法(TS 3.7 以上支持),可以避免这个错误。
// ✅ ok const name = user?.name
useReducer
需要用 Discriminated Unions 来标注 Action 的类型。
const initialState = { count: 0 }; type ACTIONTYPE = | { type: "increment"; payload: number } | { type: "decrement"; payload: string }; 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) }; default: throw new Error(); } } function Counter() { const [state, dispatch] = React.useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: "decrement", payload: "5" })}> - </button> <button onClick={() => dispatch({ type: "increment", payload: 5 })}> + </button> </> ); }
「Discriminated Unions」一般是一个联合类型,其中每一个类型都需要通过类似 type 这种特定的字段来区分,当你传入特定的 type 时,剩下的类型 payload 就会自动匹配推断。
这样:
当你写入的 type 匹配到 decrement 的时候,TS 会自动推断出相应的 payload 应该是 string 类型。
当你写入的 type 匹配到 increment 的时候,则 payload 应该是 number 类型。
这样在你 dispatch 的时候,输入对应的 type,就自动提示你剩余的参数类型啦。
useEffect
这里主要需要注意的是,useEffect 传入的函数,它的返回值要么是一个方法(清理函数),要么就是undefined,其他情况都会报错。
比较常见的一个情况是,我们的 useEffect 需要执行一个 async 函数,比如:
// ❌ // Type 'Promise<void>' provides no match // for the signature '(): void | undefined' useEffect(async () => { const user = await getUser() setUser(user) }, [])
虽然没有在 async 函数里显式的返回值,但是 async 函数默认会返回一个 Promise,这会导致 TS 的报错。
推荐这样改写:
useEffect(() => { const getUser = async () => { const user = await getUser() setUser(user) } getUser() }, [])
或者用自执行函数?不推荐,可读性不好。
useEffect(() => { (async () => { const user = await getUser() setUser(user) })() }, [])
useRef
这个 Hook 在很多时候是没有初始值的,这样可以声明返回对象中 current 属性的类型:
const ref2 = useRef(null);
以一个按钮场景为例:
function TextInputWithFocusButton() { const inputEl = React.useRef<HTMLInputElement>(null); const onButtonClick = () => { if (inputEl && inputEl.current) { inputEl.current.focus(); } }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }
当 onButtonClick 事件触发时,可以肯定 inputEl 也是有值的,因为组件是同级别渲染的,但是还是依然要做冗余的非空判断。
有一种办法可以绕过去。
const ref1 = useRef(null!);
null! 这种语法是非空断言,跟在一个值后面表示你断定它是有值的,所以在你使用 inputEl.current.focus() 的时候,TS 不会给出报错。
但是这种语法比较危险,需要尽量减少使用。
在绝大部分情况下,inputEl.current?.focus() 是个更安全的选择,除非这个值真的不可能为空。(比如在使用之前就赋值了)
useImperativeHandle
推荐使用一个自定义的 innerRef 来代替原生的 ref,否则要用到 forwardRef 会搞的类型很复杂。
type ListProps = { innerRef?: React.Ref<{ scrollToTop(): void }> } function List(props: ListProps) { useImperativeHandle(props.innerRef, () => ({ scrollToTop() { } })) return null }
结合刚刚 useRef 的知识,使用是这样的:
function Use() { const listRef = useRef<{ scrollToTop(): void }>(null!) useEffect(() => { listRef.current.scrollToTop() }, []) return ( <List innerRef={listRef} /> ) }
很完美,是不是?
自定义 Hook
如果你想仿照 useState 的形式,返回一个数组给用户使用,一定要记得在适当的时候使用 as const,标记这个返回值是个常量,告诉 TS 数组里的值不会删除,改变顺序等等……
否则,你的每一项都会被推断成是「所有类型可能性的联合类型」,这会影响用户使用。
export function useLoading() { const [isLoading, setState] = React.useState(false); const load = (aPromise: Promise<any>) => { setState(true); return aPromise.finally(() => setState(false)); }; // ✅ 加了 as const 会推断出 [boolean, typeof load] // ❌ 否则会是 (boolean | typeof load)[] return [isLoading, load] as const;[] }
对了,如果你在用 React Hook 写一个库,别忘了把类型也导出给用户使用。
forwardRef
函数式组件默认不可以加 ref,它不像类组件那样有自己的实例。这个 API 一般是函数式组件用来接收父组件传来的 ref。
所以需要标注好实例类型,也就是父组件通过 ref 可以拿到什么样类型的值。
type Props = { }; export type Ref = HTMLButtonElement; export const FancyButton = React.forwardRef<Ref, Props>((props, ref) => ( <button ref={ref} className="MyClassName"> {props.children} </button> ));
由于这个例子里直接把 ref 转发给 button 了,所以直接把类型标注为 HTMLButtonElement 即可。
父组件这样调用,就可以拿到正确类型:
export const App = () => { const ref = useRef<HTMLButtonElement>() return ( <FancyButton ref={ref} /> ) }
感谢各位的阅读,以上就是“怎么在React中完美运用”的内容了,经过本文的学习后,相信大家对怎么在React中完美运用这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。