React复杂组件重构--6种最佳实践实现高效和高可读的组件

290 阅读4分钟

问题提出

前段时间换工作以来,接手了一个比较老的react的项目,但好在版本在16以上。多次迭代之后就发现有些组件设计的只是为了完成工作而已。由此,你确定你写的react组件符合最佳实践吗?或者只是为了应付工作。

一个辣鸡 ️组件看起来是什么样子的

以下是我以前写的一个tab切换组件:

import React, { Component } from 'react';
import style from './styles.css';

class Tabs extends Component {
  constructor(props) {
    super(props)
  
    this.state = {
      currentClickedButton: ''
    }
  }
  
  render() {
    return (
      <div>
        <h3>the current clicked button is {this.state.currentClickedButton}</h3>
        <ul>
          {/* 根据eslint规范 li非交互标签不应该绑定鼠标和键盘交互事件所以包含button */}
          <li>
            <button
              onClick={() => {
                this.setState({currentClickedButton: 'Create'});
                this.props.create();
              }}
              className={style.tabsItem}
            >
              Create
            </button>
          </li>
          <li>
            <button
              onClick={() => {
                this.setState({currentClickedButton: 'Delete'});
                this.props.delete();
              }}
              className={style.tabsItem}
            >
              Delete
            </button>
          </li>
          <li>
            <button
              onClick={() => {
                this.setState({currentClickedButton: 'Update'});
                this.props.update();
              }}
              className={style.tabsItem}
            >
              Update
            </button>
          </li>
          <li>
            <button
              onClick={() => {
                this.setState({currentClickedButton: 'Reset'});
                this.props.reset();
              }}
              className={style.tabsItem}
            >
              Reset
            </button>
          </li>
        </ul>
      </div>
    )
  }
}

export default Tabs;

你会发现,简单的一个button列表组件,它只有一个功能,就是告诉你当前点击的button,却足足写了76行,但它的确是能正常运行的。让我们来改造它吧。

Step1: Hooks包装一下

然后第一次重构下来:

import React, { useState } from 'react';
import style from './styles.css';

const Tabs = props => {
  const [currentClickedButton, setCurrentClickedButton] = useState('');

  return (
    <div>
      <h3>the current clicked button is {currentClickedButton}</h3>
      <ul>
        {/* 根据eslint规范 li非交互标签不应该绑定鼠标和键盘交互事件所以包含button */}
        <li>
          <button
            onClick={() => {
              setCurrentClickedButton('Create')
              props.create();
            }}
            className={style.tabsItem}
          >
            Create
          </button>
        </li>
        <li>
          <button
            onClick={() => {
              setCurrentClickedButton('Delete')
              props.delete();
            }}
            className={style.tabsItem}
          >
            Delete
          </button>
        </li>
        <li>
          <button
            onClick={() => {
              setCurrentClickedButton('Update')
              props.update();
            }}
            className={style.tabsItem}
          >
            Update
          </button>
        </li>
        <li>
          <button
            onClick={() => {
              setCurrentClickedButton('Reset')
              props.reset();
            }}
            className={style.tabsItem}
          >
            Reset
          </button>
        </li>
      </ul>
    </div>
  )
}

export default Tabs;

使用hooks之后,直接把this全干掉了,class也干掉了,但还是有61行,继续优化。

Step2: 抽离小组件--可复用性

多看一眼我们要实现的东西,然后我们会发现,其实有很多是重复的,或者说是结构化的,现在把重复的抽出来。

import React, { useState } from 'react';
import style from './styles.css';

const ListItem = props => {
    return (
      <li>
        <button
          onClick={() => {
            props.setCurrentClicked(props.title)
            props.action();
          }}
          className={style.tabsItem}
        >
          {props.title}
        </button>
      </li>
    )
  }

const Tabs = props => {
  const [currentClickedButton, setCurrentClickedButton] = useState('');

  return (
    <div>
      <h3>the current clicked button is {currentClickedButton}</h3>
      <ul>
        {/* 根据eslint规范 li非交互标签不应该绑定鼠标和键盘交互事件所以包含button */}
        <ListItem
          title="Create"
          action={props.create}
          setCurrentClicked={setCurrentClickedButton}
        />
        <ListItem
          title="Delete"
          action={props.delete}
          setCurrentClicked={setCurrentClickedButton}
        />
        <ListItem
          title="Update"
          action={props.update}
          setCurrentClicked={setCurrentClickedButton}
        />
        <ListItem
          title="Reset"
          action={props.reset}
          setCurrentClicked={setCurrentClickedButton}
        />
      </ul>
    </div>
  )
}

export default Tabs;

这里只是将button抽出来,结构化也更明显了,可读性稍微高了点,但看着总觉得还差点什么。

Step3: Props结构化

这里主要就做一件事,需要的props值才拿,减少冗余。

import React, { useState } from 'react';
import style from './styles.css';

const ListItem = ({title, setClicked, action }) => {
  return (
    <li>
      <button
        onClick={() => {
          setClicked(title);
          action();
        }}
        className={style.tabsItem}
      >
        {title}
      </button>
    </li>
  )
}

const Tabs = ({ create, onDelete, update, reset }) => {
  // 当前点击的button
  const [clicked, setClicked] = useState('');

  return (
    <div>
      <h3>the current clicked button is {clicked}</h3>
      <ul>
        {/* 根据eslint规范 li非交互标签不应该绑定鼠标和键盘交互事件所以包含button */}
        <ListItem title="Create" action={create} setClicked={setClicked} />
        <ListItem title="Delete" action={onDelete} setClicked={setClicked} />
        <ListItem title="Update" action={update} setClicked={setClicked} />
        <ListItem title="Reset" action={reset} setClicked={setClicked} />
      </ul>
    </div>
  )
}

export default Tabs;

其实这里的listItem是可以用map做一个结构化的,主要看Item扩展性要求高不高。

Step4: 数据驱动 vs Props结构化

我们都知道React是数据驱动的,这里是不是可以使data更结构化一点呢?这里不经意间想起了Vuedata。而且props结构化会在扩展性方面会有所欠缺,或者props要传入的参数很多,那可能就不适合用该方法。所以这里考虑下可复用性和可扩展性。

import React, { useState } from 'react';
import PropTypes from 'prop-types';

function TabsList(props) {
  const { items, onClick, className } = props;
  
  function onItemClick(item) {
    if (item.action) item.action(item);
    if (onClick) props.onClick(item);
  }
  
  return (
    <ul className={className}>
      {items.map(item => (
        <li key={item.title}>
          <button onClick={() => onItemClick(item)}>
            {item.title}
          </button>
        </li>
      ))}
    </ul>
  )
}

TabsList.propTypes = {
  items: PropTypes.array,
  title: PropTypes.string,
  onClick: PropTypes.func,
}

function CustomTabs(props) {
  const { create, onDelete, update, reset} = props;
  const [itemClicked, setItemClicked] = useState({});

  const items = [
    { title: 'Create',  action: create },
    { title: 'Delete',  action: onDelete },
    { title: 'Update',  action: update },
    { title: 'Reset',  action: reset },
  ];

  return (
    <div>
      <h3>the current clicked button is {itemClicked.title}</h3>
      <TabsList 
        items={items} 
        onClick={setItemClicked} 
        className={props.listClass}
      />
    </div>
  )
}

CustomTabs.propTypes = {
  listClass: PropTypes.string,
}

export default CustomTabs;

经过这次重构:

  • 可复用性提高
  • 更简单
  • 扩展性更好
  • data总揽全局

Step5: 添加PropTypes类型校验

添加类型校验主要是为了避免类型传错时引起的报错。

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import style from './styles.css';

const ListItem = ({title, setClicked, action }) => {
  return (
    <li>
      <button
        onClick={() => {
          setClicked(title);
          action();
        }}
        className={style.tabsItem}
      >
        {title}
      </button>
    </li>
  )
}

ListItem.propTypes = {
  title: PropTypes.string,
  setClicked: PropTypes.func,
  action: PropTypes.func,
}

const Tabs = ({ create, onDelete, update, reset }) => {
  // 当前点击的button
  const [clicked, setClicked] = useState('');

  return (
    <div>
      <h3>the current clicked button is {clicked}</h3>
      <ul>
        {/* 根据eslint规范 li非交互标签不应该绑定鼠标和键盘交互事件所以包含button */}
        <ListItem title="Create" action={create} setClicked={setClicked} />
        <ListItem title="Delete" action={onDelete} setClicked={setClicked} />
        <ListItem title="Update" action={update} setClicked={setClicked} />
        <ListItem title="Reset" action={reset} setClicked={setClicked} />
      </ul>
    </div>
  )
}

Tabs.propTypes = {
  create: PropTypes.func,
  onDelete: PropTypes.func,
  update: PropTypes.func,
  reset: PropTypes.func,
}

export default Tabs;

Step6: 拆分小组件(单一原则)

这里TabsListItem分开两个组件文件写即可,就不贴出来了。

总结

比较以下最初的组件,我们收获了更高效和可读性更高的组件。再进一步的话我们可以为其编写单元测试用例,这个下回分解。

全文完,欢迎交流讨论。如果觉得写得还不错,那就点个赞吧。

如果转载本文,文本务必注明:“转自知乎作者:大猪蹄子研究院“