Skip to content

React 进阶之路

本章节主要根据项目实践。总结了一些React 组件设计思想。有助于你写出更好更健壮的React项目

通用

类型命名方式

interface、type、enum命名方式

社区对于ts类型命名有两种流派:

ts
interface ComponentNameProps {}

type ComponentNamePropsType = { }

enum ComponentNameEnum = {}

ts
interface IComponentNameProps {}

type TComponentNameProps = { }

enum EComponentName = {}

相比较来说。公司更建议使用下面的这种方式。原因是

  • 类型别称缩写开头。更容易区分定义的规则
  • 命名更简短
  • 引用索引更快。因为你只需敲击一个E或者I

使用handleEvent命名事件处理器

对于事件处理函数。安装按照handle{Type}{Event}命名, 例如 handleNameChange

tsx
export const EventDemo: FC<{}> = props => {
  const handleClick = useCallback<React.MouseEventHandler>(evt => {
    evt.preventDefault();
    // ...
  }, []);

  return <button onClick={handleClick} />;
};

很多同学在开发项目时。为了快。经常会直接用onxxx的写法。像这样:

tsx
export const EventDemo: FC<{}> = props => {
  // 这样写虽然快。但是并不优雅。
  const onClick = useCallback<React.MouseEventHandler>(evt => {  
    evt.preventDefault();
    // ...
  }, []);

  return <button onClick={onClick} />;  
};

虽然这么写看起来会快一些。但是,其实并不符合规范。我们希望你在短暂的开发生涯中。能往更高级的level进发。那么首先就从遵循社区规范共识开始把!

自定义组件暴露事件处理器类型

自定义组件的编写很多时候也会定义暴露自己的事件处理器类型, 尤其是较为复杂的事件处理器, 这样可以避免开发者手动为每个事件处理器的参数声明类型

自定义事件处理器类型以{ComponentName}{Event}Handler命名. 这是为了和原生事件处理器类型区分, 不使用EventHandler形式的后缀

举个例子:

tsx
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 进行解析. 这个工具对我们的组件注释有一定的要求

tsx
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 组件的逻辑,而不需要关心它在其他组件中的具体使用情况。

tsx
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并导出

tsx

/**
 * 组件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)

tsx
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。那么你可以这样

tsx
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更建议在组件内解构处理。像这样

tsx
export interface IHelloProps {
  name?: string; // 可选属性
}

// 利用对象默认属性值语法
export const Hello: FC<IHelloProps> = (props) => {
  const { name = 'namehu' } = props
  
  return <div>Hello {name}!</div>
}

这样做有一个缺点。那就是name 类型如果定义为 string | null。那么传递null。会导致默认值失效. 所以你也可以这样,虽然不是特别建议

tsx
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

tsx
export default (props: {}) => {
  return <div>hello react</div>;
};

实在要导出。你可以这样

tsx
export default function Foo(props: {}) {
  return <div>xxx</div>;
}

父子组件

当你在使用一些三方库。如antd时。你会发现他们组件有这种用法

tsx
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更为优雅些. 当然也有可能让代码变得啰嗦.

组件的编写规则大概类似于这样

tsx
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>;

泛型组件

泛型在一下列表型或容器型的组件中比较常用

tsx
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有了可能。 并且相比较于类组件。控制粒度更细

tsx
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。会帮你过滤掉很多无谓的更新
  • 不要过度迷信自己的代码能力
tsx
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

见上面的例子

子组件声明

类组件可以使用静态属性形式声明子组件

tsx
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>;
  }
}

泛型 组件

tsx
export class List<T> extends React.PureComponent<ListProps<T>> {
  public render() {
    return null
  }
}