问题提出
前段时间换工作以来,接手了一个比较老的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更结构化一点呢?这里不经意间想起了Vue的data。而且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: 拆分小组件(单一原则)
这里Tabs和ListItem分开两个组件文件写即可,就不贴出来了。
总结
比较以下最初的组件,我们收获了更高效和可读性更高的组件。再进一步的话我们可以为其编写单元测试用例,这个下回分解。
全文完,欢迎交流讨论。如果觉得写得还不错,那就点个赞吧。
如果转载本文,文本务必注明:“转自知乎作者:大猪蹄子研究院“