React 进阶之路
本章节主要根据项目实践。总结了一些React 组件设计思想。有助于你写出更好更健壮的React项目
通用
类型命名方式
interface、type、enum命名方式
社区对于ts类型命名有两种流派:
interface ComponentNameProps {}
type ComponentNamePropsType = { }
enum ComponentNameEnum = {}
与
interface IComponentNameProps {}
type TComponentNameProps = { }
enum EComponentName = {}
相比较来说。公司更建议使用下面的这种方式。原因是
- 类型别称缩写开头。更容易区分定义的规则
- 命名更简短
- 引用索引更快。因为你只需敲击一个E或者I
使用handleEvent命名事件处理器
对于事件处理函数。安装按照handle{Type}{Event}
命名, 例如 handleNameChange
export const EventDemo: FC<{}> = props => {
const handleClick = useCallback<React.MouseEventHandler>(evt => {
evt.preventDefault();
// ...
}, []);
return <button onClick={handleClick} />;
};
很多同学在开发项目时。为了快。经常会直接用onxxx
的写法。像这样:
export const EventDemo: FC<{}> = props => {
// 这样写虽然快。但是并不优雅。
const onClick = useCallback<React.MouseEventHandler>(evt => {
evt.preventDefault();
// ...
}, []);
return <button onClick={onClick} />;
};
虽然这么写看起来会快一些。但是,其实并不符合规范。我们希望你在短暂的开发生涯中。能往更高级的level进发。那么首先就从遵循社区规范共识开始把!
自定义组件暴露事件处理器类型
自定义组件的编写很多时候也会定义暴露自己的事件处理器类型, 尤其是较为复杂的事件处理器, 这样可以避免开发者手动为每个事件处理器的参数声明类型
自定义事件处理器类型以{ComponentName}{Event}Handler
命名. 这是为了和原生事件处理器类型区分, 不使用EventHandler形式的后缀
举个例子:
import React, { FC, useState } from 'react';
export interface UploadValue {
url: string;
name: string;
size: number;
}
/**
* 暴露事件处理器类型
*/
export type UploadChangeHandler = (value?: UploadValue, file?: File) => void;
export interface UploadProps {
value?: UploadValue;
onChange?: UploadChangeHandler;
}
export const Upload: FC<UploadProps> = props => {
return <div>...</div>;
};
通用组件写好注释
当你在下项目时或者多人合作开发时。有时会编写一些通用组件。或者发布至NPM平台。那么你就需要提供良好的组件注释。
目前社区有多种 react 组件文档生成方案, 例如docz, styleguidist还有storybook.
它们底层都使用react-docgen-typescript对 Typescript 进行解析. 这个工具对我们的组件注释有一定的要求
import React, { Component } from 'react';
/**
* Props注释
* @description 详细描述
*/
export interface IColumnProps extends React.HTMLAttributes<any> {
/** prop1 description */
prop1?: string;
/** prop2 description */
prop2: number;
/**
* prop3 description
*/
prop3: () => void;
/** prop4 description */
prop4: 'option1' | 'option2' | 'option3';
}
/**
* 对组件进行注释
*/
export class Column extends Component<IColumnProps, {}> {
render() {
return <div>Column</div>;
}
}
Render Props
得益于Reac和JSX的极大灵活性。React的props并没有限定类型, 它可以是一个函数. 于是就有了 render props, 这是和高阶组件一样组件复用常见的设计模式:
它的特点有:
代码复用:Render Props 模式允许你将可复用的代码逻辑封装在一个组件中,并通过将该组件作为 prop 渲染到其他组件中来共享该逻辑。这样可以避免代码重复,提高代码的可维护性和可扩展性。
灵活性:Render Props 模式提供了一种灵活的方式来共享状态和逻辑。通过传递一个函数作为 prop,该函数可以获取一些数据或行为,并将其渲染到子组件中。这使得组件可以根据需要自定义渲染以满足具体的业务需求。
组件解耦:Render Props 模式可以帮助解耦组件之间的依赖关系。通过将共享的逻辑封装在一个 Render Props 组件中,其他组件可以直接使用该组件的渲染 prop 来获取所需的数据或行为,而不必关心内部实现细节。
数据传递:借助 Render Props 模式,可以将数据从父组件传递到子组件,从而实现数据的共享和传递。这使得在组件树中的任何层级都可以访问和操作数据,而不需要通过多层的 prop 传递。
可测试性:由于 Render Props 模式将逻辑封装在可复用的组件中,这使得测试变得更加容易。你可以单独测试 Render Props 组件的逻辑,而不需要关心它在其他组件中的具体使用情况。
import React, { ReactNode } from 'react';
export interface IThemeConsumerProps {
/**
* 其他的命名方式一般为renderxxxx开头
*/
renderHeader?: (theme: ITheme) => ReactNode;
/**
* 你可以直接把children处理成render模式
*/
children: (theme: ITheme) => ReactNode;
}
interface ITheme {
primary: string;
secondary: string;
}
export const ThemeConsumer = (props: IThemeConsumerProps) => {
const innerTheme:ITheme = { primary: 'red', secondary: 'blue' };
return (
<div>
{props.renderHeader(innerTheme);}
{props.children(innerTheme);}
</div>
)
};
// Test.tsx
<ThemeConsumer
renderHeader={({ primary }) => {
return <div style={{ color: primary }} />;
}}
>
{({ primary }) => {
return <div style={{ color: primary }} />;
}}
</ThemeConsumer>;
函数组件
子聪React Hooks 出现后, 函数组件渐渐成为React组件的一等公民。不管是官方还是社区都在积极使用函数组件。并进行了一场浩浩荡荡的组件从类组件改写函数组件的进程。
时至今日。我们也逐渐习惯默认使用函数组件来编写React代码。所以。针对函数组件的写法。我们提在了最前面。
在组件中定义props并导出
/**
* 组件props类型
*/
export interface IComponentNameProps {
/** 样式 */
className?: string
}
/**
* 组件
*/
const Component: FC<IComponentNameProps> = (props) =>{
return <div className={props.className}>123</div>
}
const ComponentName = memo(Component)
export default ComponentName
使用FC类型来声明函数组件
FC是FunctionComponent的简写, 这个类型定义了默认的 props以及一些静态属性(如 defaultProps)
import React, { FC, CSSProperties } from 'react';
/**
* 声明Props类型
*/
export interface IMyComponentProps {
className?: string;
style?: CSSProperties;
}
export const MyComponent: FC<IMyComponentProps> = props => {
return <div>hello react</div>;
};
有时候你会发现上面的组件不支持你写children。那么你可以这样
import React, { FC, CSSProperties, PropsWithChildren } from 'react';
/**
* 声明Props类型
*/
export interface IMyComponentProps {
className?: string;
style?: CSSProperties;
}
export const MyComponent: FC<PropsWithChildren<IMyComponentProps>> = props => {
return <div>{props.children}</div>;
};
defaultProps 声明
函数的defaultProps
更建议在组件内解构处理。像这样
export interface IHelloProps {
name?: string; // 可选属性
}
// 利用对象默认属性值语法
export const Hello: FC<IHelloProps> = (props) => {
const { name = 'namehu' } = props
return <div>Hello {name}!</div>
}
这样做有一个缺点。那就是name 类型如果定义为 string | null
。那么传递null。会导致默认值失效. 所以你也可以这样,虽然不是特别建议
import React, { FC } from 'react';
export interface IHelloProps {
name: string;
}
export const Component: FC<IHelloProps> = ({ name }) => <div>Hello {name}!</div>;
Component.defaultProps = { name: 'namehu' };
const ComponentName = memo(Component)
export default ComponentName
不要导出默认箭头函数组件
这种方式导出的组件在React Inspector查看时会显示为Unknown
export default (props: {}) => {
return <div>hello react</div>;
};
实在要导出。你可以这样
export default function Foo(props: {}) {
return <div>xxx</div>;
}
父子组件
当你在使用一些三方库。如antd
时。你会发现他们组件有这种用法
import React from 'react';
import { Space, Typography } from 'antd';
const { Text } = Typography; // 解构子组件
const App: React.FC = () => (
<Space direction="vertical">
<Text type="secondary">Ant Design (secondary)</Text>
// 直接调用子组件
<Typography.Link href="https://ant.design" target="_blank">
Ant Design (Link)
</Typography.Link>
</Space>
);
export default App;
这种使用Parent.Child形式的 JSX 可以让节点父子关系更加直观, 它类似于一种命名空间的机制, 可以避免命名冲突
相比ParentChild这种命名方式, Parent.Child更为优雅些. 当然也有可能让代码变得啰嗦.
组件的编写规则大概类似于这样
import React, { PropsWithChildren } from 'react';
export interface ILayoutProps {}
export interface ILayoutHeaderProps {} // 采用IParentChildProps形式命名
export interface ILayoutFooterProps {}
function Component(props: PropsWithChildren<ILayoutProps>) {
return <div className="layout">{props.children}</div>;
}
const Header: FC<PropsWithChildren<ILayoutHeaderProps>> = (props) => {
return <div className="header">{props.children}</div>;
};
const Footer: PropsWithChildren<ILayoutFooterProps> = (props) => {
return <div className="footer">{props.children}</div>;
};
// 这里你可能需要处理一下类型
const Layout = memo(Component) as unknown as typeof Component & {
Header: typeof Header
Footer: typeof Footer
}
// 作为父组件的属性挂载
Layout.Header = Header
Layout.Footer = Footer
// 导出
export default Layout
// Test.jsx 使用
<Layout>
<Layout.Header>header</Layout.Header>
<Layout.Footer>footer</Layout.Footer>
</Layout>;
泛型组件
泛型在一下列表型或容器型的组件中比较常用
import React from 'react';
export interface IListProps<T> {
visible: boolean;
list: T[];
renderItem: (item: T, index: number) => React.ReactNode;
}
export function List<T>(props: IListProps<T>) {
return <div />;
}
// Test
function Test() {
return (
<List
list={[1, 2, 3]}
renderItem={item => {
/* item会自动推断i为number类型*/
}}
/>
);
}
Forwarding Refs
React 在 16.8.4 支持了 forwardRef和useImperativeHandle, 用于转发ref, 适用于 HOC 和函数组件。这个特性给函数组件绑定Ref有了可能。 并且相比较于类组件。控制粒度更细
import MMDialog from '.'
import { IDialogProps } from './const'
import { useImperativeHandle, useState, forwardRef, memo } from 'react'
type IContainerDialogProps = Omit<IDialogProps, 'visible'>
export interface IContainerDialogRef {
/**
* 显示弹窗
* @param option
*/
show(option: IContainerDialogProps): void
}
/**
* 对话框容器组件
*
* 对外暴露通用show方法来使用
*/
const Component = forwardRef<IContainerDialogRef, IContainerDialogProps>((props, ref) => {
const initProps = () => ({ ...props, visible: false })
const [dialogProps, setDialogProps] = useState(initProps)
useImperativeHandle(
ref,
() => {
function hide() {
setDialogProps(initProps())
}
return {
show(option: Omit<IDialogProps, 'visible'>) {
const { onCancel, onOk, ...rest } = option
setDialogProps((pre) => ({
...pre,
...rest,
visible: true,
onCancel: () => {
hide()
onCancel?.()
},
onOk: async () => {
try {
await onOk?.()
hide()
} catch (error) {}
}
}))
}
}
},
[props]
)
return <MMDialog {...dialogProps} />
})
const ContainerDialog = memo(Component)
export default ContainerDialog
类组件
类组件是React最早的一种组件编写方式。直到现在以及未来。官方依旧会保持类组件的支持。虽然作为新项目的开发。我们已经不太使用。但是对于你维护老项目或者查看其他开源项目时。这些规范也会给你提供一些帮助
全部继承 PureComponent
永远使用PureComponent而不是Component。至于原因:
- PureComponet 自带更新differ。会帮你过滤掉很多无谓的更新
- 不要过度迷信自己的代码能力
import React from 'react';
export interface ICounterProps {
defaultCount: number; // 可选props, 不需要?修饰
}
/**
* 组件状态
*/
interface IState {
count: number;
}
/**
* 类注释
* 继承React.PureComponent, 并声明Props和State类型
*/
export class Counter extends React.PureComponent<ICounterProps, IState> {
/**
* 默认参数
*/
public static defaultProps = {
defaultCount: 0,
};
/**
* 初始化State
*/
public state = {
count: this.props.defaultCount,
};
/**
* 声明周期方法
*/
public componentDidMount() {}
/**
* 建议靠近componentDidMount, 资源消费和资源释放靠近在一起, 方便review
*/
public componentWillUnmount() {}
public componentDidCatch() {}
public componentDidUpdate(prevProps: ICounterProps, prevState: IState) {}
/**
* 渲染函数
*/
public render() {
return (
<div>
{this.state.count}
<button onClick={this.increment}>Increment</button>
<button onClick={this.decrement}>Decrement</button>
</div>
);
}
/**
* ① 组件私有方法, 不暴露
* ② 使用类实例属性+箭头函数形式绑定this
*/
private increment = () => {
this.setState(({ count }) => ({ count: count + 1 }));
};
private decrement = () => {
this.setState(({ count }) => ({ count: count - 1 }));
};
}
使用static defaultProps定义默认 props
见上面的例子
子组件声明
类组件可以使用静态属性形式声明子组件
export class Layout extends React.PureComponent<ILayoutProps> {
public static Header = Header;
public static Footer = Footer;
public render() {
return <div className="layout">{this.props.children}</div>;
}
}
泛型 组件
export class List<T> extends React.PureComponent<ListProps<T>> {
public render() {
return null
}
}