从一个简单功能的实现,谈谈 react 中的逻辑复用进化过程

需求:我们现在有一个获取验证码的按钮,需要在点击后禁用,并且在按钮上显示倒计时60秒才可以进行第二次点击。
本篇文章通过对这个需求的八种实现方式来讨论在 react 中的逻辑复用的进化过程

代码例子放在了 codesandbox 上。

方案一 使用 setInterval

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import React from 'react'

export default class LoadingButtonInterval extends React.Component {
state = {
loading: false,
btnText: '获取验证码',
totalSecond: 10
}
timer = null
componentWillUnmount() {
this.clear()
}
clear = () => {
clearInterval(this.timer)

this.setState({
loading: false,
totalSecond: 10
})
}
setTime = () => {
this.timer = setInterval(() => {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState(() => ({
totalSecond: totalSecond - 1
}))
}, 1000)
}
onFetch = () => {
this.setState(() => ({ loading: true }))
const { totalSecond } = this.state
this.setState(() => ({
totalSecond: totalSecond - 1
}))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<button disabled={loading} onClick={this.onFetch}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)
}
}

方案二 使用 setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import React from 'react'

export default class LoadingButton extends React.Component {
state = {
loading: false,
btnText: '获取验证码',
totalSecond: 60
}
timer = null

componentWillUnmount() {
this.clear()
}
clear = () => {
clearTimeout(this.timer)

this.setState({
loading: false,
totalSecond: 60
})
}
setTime = () => {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState({
totalSecond: totalSecond - 1
})
this.timer = setTimeout(() => {
this.setTime()
}, 1000)
}
onFetch = () => {
this.setState(() => ({ loading: true }))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<button disabled={loading} onClick={this.onFetch}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)
}
}

我们可能很快就写出来两个这样的组件。使用 setTimeout 还是 setInterval 区别不是特别大。 但是我会更推荐 setTimeout 因为 万物皆递归(逃)

不过,又有更高的要求了。可以看到刚刚我们的获取验证码。如果说再有一个页面有相同的需求,只能将组件完全再拷贝一遍。这肯定不合适嘛。

那咋办嘛?

方案三 参数提取到 Props 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import React from "react";
class LoadingButtonProps extends React.Component {
constructor(props) {
super(props);
this.initState = {
loading: false,
btnText: this.props.btnText || "获取验证码",
totalSecond: this.props.totalSecond || 60
};
this.state = { ...this.initState };
}
timer = null;
componentWillUnmount() {
this.clear();
}
clear = () => {
clearTimeout(this.timer);
this.setState({
...this.initState
});
};
setTime = () => {
const { totalSecond } = this.state;
if (totalSecond <= 0) {
this.clear();
return;
}
this.setState({
totalSecond: totalSecond - 1
});
this.timer = setTimeout(() => {
this.setTime();
}, 1000);
};
onFetch = () => {
const { loading } = this.state;
if (loading) return;
this.setState(() => ({ loading: true }));
this.setTime();
};
render() {
const { loading, btnText, totalSecond } = this.state;
return (
<button disabled={loading} onClick={this.onFetch}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
);
}
}

class LoadingButtonProps1 extends React.Component {
render() {
return <LoadingButtonProps btnText={"获取验证码1"} totalSecond={10} />;
}
}
class LoadingButtonProps2 extends React.Component {
render() {
return <LoadingButtonProps btnText={"获取验证码2"} totalSecond={20} />;
}
}

export default () => (
<div>
<LoadingButtonProps1 />
<LoadingButtonProps2 />
</div>
);

对于上面的需求,不就是复用嘛,看我 props 提取到公共父组件一把梭搞定!
想想好像还挺美的。。

结果这时候需求变更来了:

第一点:两个地方获取验证码的api不一样。第二点:我需要在获取验证码之前做一些别的事情

挠了挠头,那咋办嘛?

方案四 参数提取到 Props 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import React from 'react'

class LoadingButtonProps extends React.Component {
// static defaultProps = {
// loading: false,
// btnText: '获取验证码',
// totalSecond: 10,
// onStart: () => {},
// onTimeChange: () => {},
// onReset: () => {}
// }
timer = null
componentWillUnmount() {
this.clear()
}
clear = () => {
clearTimeout(this.timer)
this.props.onReset()
}
setTime = () => {
const { totalSecond } = this.props
console.error(totalSecond)
if (this.props.totalSecond <= 0) {
this.clear()
return
}
this.props.onTimeChange()
this.timer = setTimeout(() => {
this.setTime()
}, 1000)
}
onFetch = () => {
if (this.loading) return
this.setTime()
this.props.onStart()
}
render() {
return <div onClick={this.onFetch}>{this.props.children}</div>
}
}

class LoadingButtonProps1 extends React.Component {
totalSecond = 10
state = {
loading: false,
btnText: '获取验证码1',
totalSecond: this.totalSecond
}
onTimeChange = () => {
const { totalSecond } = this.state
this.setState(() => ({ totalSecond: totalSecond - 1 }))
}
onReset = () => {
this.setState({
loading: false,
totalSecond: this.totalSecond
})
}
onStart = () => {
this.setState(() => ({ loading: true }))
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<LoadingButtonProps
loading={loading}
totalSecond={totalSecond}
onStart={this.onStart}
onTimeChange={this.onTimeChange}
onReset={this.onReset}
>
<button disabled={loading}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
</LoadingButtonProps>
)
}
}
class LoadingButtonProps2 extends React.Component {
totalSecond = 15
state = {
loading: false,
btnText: '获取验证码2',
totalSecond: this.totalSecond
}
onTimeChange = () => {
const { totalSecond } = this.state
this.setState(() => ({ totalSecond: totalSecond - 1 }))
}
onReset = () => {
this.setState({
loading: false,
totalSecond: this.totalSecond
})
}
onStart = () => {
this.setState(() => ({ loading: true }))
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<LoadingButtonProps
loading={loading}
totalSecond={totalSecond}
onStart={this.onStart}
onTimeChange={this.onTimeChange}
onReset={this.onReset}
>
<button disabled={loading}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
</LoadingButtonProps>
)
}
}
export default () => (
<div>
<LoadingButtonProps1 />
<LoadingButtonProps2 />
</div>
)

嗯?等等。。所以说这样的操作只共用了时间递归减少的部分吧?好像重复代码有点多哇,感觉和老版本也没什么太大的区别嘛。

那咋办嘛?

方案五 试试 HOC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import React from 'react'

function loadingButtonHoc(WrappedComponent, initState) {
return class extends React.Component {
constructor(props) {
super(props)
this.initState = initState || {
loading: false,
btnText: '获取验证码',
totalSecond: 60
}
this.state = { ...this.initState }
}
timer = null

componentWillUnmount() {
this.clear()
}
clear = () => {
clearTimeout(this.timer)

this.setState({
...this.initState
})
}
setTime = () => {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState({
totalSecond: totalSecond - 1
})
this.timer = setTimeout(() => {
this.setTime()
}, 1000)
}
onFetch = () => {
const { loading } = this.state
if (loading) return
this.setState(() => ({ loading: true }))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<WrappedComponent
{...this.props}
onClick={this.onFetch}
loading={loading}
btnText={btnText}
totalSecond={totalSecond}
/>
)
}
}
}
class LoadingButtonHocComponent extends React.Component {
render() {
const { loading, btnText, totalSecond, onClick } = this.props
return (
<button disabled={loading} onClick={onClick}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)
}
}
const LoadingButtonHocComponent1 = loadingButtonHoc(LoadingButtonHocComponent, {
loading: false,
btnText: '获取验证码Hoc1',
totalSecond: 20
})
const LoadingButtonHocComponent2 = loadingButtonHoc(LoadingButtonHocComponent, {
loading: false,
btnText: '获取验证码Hoc2',
totalSecond: 12
})
export default () => (
<div>
<LoadingButtonHocComponent1 />
<LoadingButtonHocComponent2 />
</div>
)

我们使用 高阶组件再次重写了整个逻辑。好像基本上需求都满足了?
这个地方思路在于,将 onClick 或者叫做 onStart 事件暴露出来了,最终的执行,
都是由外部组件自行决定执行时机,那么其实不管怎么搞都可以了

方案六 renderProps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

import React from 'react'
class LoadingButtonRenderProps extends React.Component {
constructor(props) {
super(props)
this.initState = {
loading: false,
btnText: this.props.btnText || '获取验证码',
totalSecond: this.props.totalSecond || 60
}
this.state = { ...this.initState }
}
timer = null

componentWillUnmount() {
this.clear()
}
clear = () => {
clearTimeout(this.timer)

this.setState({
...this.initState
})
}
setTime = () => {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState({
totalSecond: totalSecond - 1
})
this.timer = setTimeout(() => {
this.setTime()
}, 1000)
}
onFetch = () => {
const { loading } = this.state
if (loading) return
this.setState(() => ({ loading: true }))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return this.props.children({
onClick: this.onFetch,
loading: loading,
btnText: btnText,
totalSecond: totalSecond
})
}
}
class LoadingButtonRenderProps1 extends React.Component {
render() {
return (
<LoadingButtonRenderProps btnText={'获取验证码RP1'} totalSecond={15}>
{({ loading, btnText, totalSecond, onClick }) => (
<button disabled={loading} onClick={onClick}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)}
</LoadingButtonRenderProps>
)
}
}
class LoadingButtonRenderProps2 extends React.Component {
render() {
return (
<LoadingButtonRenderProps btnText={'获取验证码RP1'} totalSecond={8}>
{({ loading, btnText, totalSecond, onClick }) => (
<button disabled={loading} onClick={onClick}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)}
</LoadingButtonRenderProps>
)
}
}

export default () => (
<div>
<LoadingButtonRenderProps1 />
<LoadingButtonRenderProps2 />
</div>
)

嘿嘿,我们使用了 render Props 重写了在 Hoc 上实现的功能。个人角度看,其实比Hoc 会简洁也优雅很多!

方案七 React Hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React, { useState, useEffect, useRef, useCallback } from 'react'
function LoadingButtonHooks(props) {
const timeRef = useRef(null)
const [loading, setLoading] = useState(props.loading)
const [btnText, setBtnText] = useState(props.btnText)
const [totalSecond, setTotalSecond] = useState(props.totalSecond)
const countRef = useRef(totalSecond)
const clear = useCallback(() => {
clearTimeout(timeRef.current)
setLoading(false)
setTotalSecond(props.totalSecond)
countRef.current = props.totalSecond
})
const setTime = useCallback(() => {
if (countRef.current <= 0) {
clear()
return
}
countRef.current = countRef.current - 1
setTotalSecond(countRef.current)

timeRef.current = setTimeout(() => {
setTime()
}, 1000)
})
const onStart = useCallback(() => {
if (loading) return
countRef.current = totalSecond
setLoading(true)
setTime()
})

useEffect(() => {
return () => {
clearTimeout(timeRef.current)
}
}, [])
return (
<button disabled={loading} onClick={onStart}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)
}
LoadingButtonHooks.defaultProps = {
loading: false,
btnText: '获取验证码',
totalSecond: 10
}
export default () => (
<div>
<LoadingButtonHooks
loading={false}
btnText={'获取验证码hooks1'}
totalSecond={10}
/>
<LoadingButtonHooks
loading={false}
btnText={'获取验证码hooks2'}
totalSecond={11}
/>
</div>
)

我们使用 hooks 重写了整个程序, 它让我们把ui和状态更明确的区分开,也去解决了一些 renderProps 在多层嵌套时的jsx 嵌套地狱问题, 当然个人感觉在这个例子上好像 Hooks 与 renderProps 版本是差别不大的。

方案八 uesHooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import React, { useState, useEffect, useRef, useCallback } from 'react'
function useLoadingTimer(initState) {
const timeRef = useRef(null)
const [loading, setLoading] = useState(initState.loading)
const [btnText, setBtnText] = useState(initState.btnText)
const [totalSecond, setTotalSecond] = useState(initState.totalSecond)
const countRef = useRef(totalSecond)
const clear = useCallback(() => {
clearTimeout(timeRef.current)
setLoading(false)
setTotalSecond(initState.totalSecond)
countRef.current = initState.totalSecond
})
const setTime = useCallback(() => {
if (countRef.current <= 0) {
clear()
return
}
countRef.current = countRef.current - 1
setTotalSecond(countRef.current)

timeRef.current = setTimeout(() => {
setTime()
}, 1000)
})
const onStart = useCallback(() => {
if (loading) return
countRef.current = totalSecond
setLoading(true)
setTime()
})

useEffect(() => {
return () => {
clearTimeout(timeRef.current)
}
}, [])
return {
onStart,
loading,
totalSecond,
btnText
}
}
const LoadingButtonHooks1 = () => {
const { onStart, loading, totalSecond, btnText } = useLoadingTimer({
loading: false,
btnText: '获取验证码UseHooks1',
totalSecond: 10
})
return (
<button disabled={loading} onClick={onStart}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)
}
const LoadingButtonHooks2 = () => {
const { onStart, loading, totalSecond, btnText } = useLoadingTimer({
loading: false,
btnText: '获取验证码UseHooks2',
totalSecond: 10
})
return (
<button disabled={loading} onClick={onStart}>
{!loading ? btnText : `请等待${totalSecond}秒..`}
</button>
)
}
export default () => (
<div>
<LoadingButtonHooks1 />
<LoadingButtonHooks2 />
</div>
)

当然,更解耦的做法是,把 hooks 完全独立的提取出来成 useHooks ,最后我们再编写组件去组合 uesHooks。

在上述的例子中我们在 react 中用了 8 种 不同的方案,去描述了同一个功能的编写过程。有一点 “回” 字的多种写法的意味。不过他也代表着 react 社区在选择实现上的思想的变化过程,我觉得谈不上某一个方案,一定就完全比另外一个好。社区也有比如 HOC vs renderProps 的很多讨论。

仅以此希望大家能够辩证的去看这个过程,也希望能够在大家编写 React 组件时带来更多的新思路。

参考链接:

坚持原创技术分享,您的支持将鼓励我继续创作!