使用ReactHook重寫ClassComponent

前言

在 React16.8 中,新增hook功能,能夠在函數組件(Function Component)中使用 state 及其它原本在類組件才能使用的功能。今日我們來試一下怎麼把一個類組件(Class Component)使用新的 Hook 功能改寫成函數組件。

為甚麼要用函數組件

函數組件和類組件最大的區別在於語法,函數組件就是一個函數接受props作為參數,返回 React Element。

1
const Home = props => <div>Welcome, ${props.name}</div>;

而類組件則需要繼承 React.Component,並實現 render 方法。與函數組件相比,在代碼簡潔程度來說,類組件略遜一籌。

1
2
3
4
5
class Home extends React.Component {
render() {
return <div>Welcome, ${props.name}</div>;
}
}

但在 React16.8 之前,函數組件一個致命的弱點,函數組件無法使用 state、refs 和生命週期鈎子,如果要使用這些功能,就必須寫成類組件。

在一些 React 的最佳實踐中推薦能使用函數組件就使用函數組件,當需要用到 state、refs 和生命週期鈎子時才改為類組件。

原因包括:

  • 函數組件代碼更簡潔
  • 因為沒有生命週期和狀態,函數組件更容易測試和閱讀(React16.8前)
  • 更符合最佳實踐中,把組件分為展示組件(Presentation Component)和容器組件(Container Component)的理念

要改寫的目標

可以來試試改改傳說 Antd 的組件,挑了一個相對簡單的 BackTop 組件來改。
GitHub: 點我

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
function getDefaultTarget() {
return window;
}

export interface BackTopProps {
visibilityHeight?: number;
onClick?: React.MouseEventHandler<HTMLElement>;
target?: () => HTMLElement | Window;
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
visible?: boolean; // Only for test. Don't use it.
}

export default class BackTop extends React.Component<BackTopProps, any> {
static defaultProps = {
visibilityHeight: 400
};

scrollEvent: any;

constructor(props: BackTopProps) {
super(props);
this.state = {
visible: false
};
}

componentDidMount() {
const getTarget = this.props.target || getDefaultTarget;
this.scrollEvent = addEventListener(
getTarget(),
"scroll",
this.handleScroll
);
this.handleScroll();
}

componentWillUnmount() {
if (this.scrollEvent) {
this.scrollEvent.remove();
}
}

scrollToTop = (e: React.MouseEvent<HTMLDivElement>) => {
const { target = getDefaultTarget, onClick } = this.props;
scrollTo(0, {
getContainer: target
});
if (typeof onClick === "function") {
onClick(e);
}
};

handleScroll = () => {
const { visibilityHeight, target = getDefaultTarget } = this.props;
const scrollTop = getScroll(target(), true);
this.setState({
visible: scrollTop > (visibilityHeight as number)
});
};

renderBackTop = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
className = "",
children
} = this.props;
const prefixCls = getPrefixCls("back-top", customizePrefixCls);
const classString = classNames(prefixCls, className, {
[`${prefixCls}-rtl`]: direction === "rtl"
});

const defaultElement = (
<div className={`${prefixCls}-content`}>
<div className={`${prefixCls}-icon`} />
</div>
);

// fix https://fb.me/react-unknown-prop
const divProps = omit(this.props, [
"prefixCls",
"className",
"children",
"visibilityHeight",
"target",
"visible"
]);

const visible =
"visible" in this.props ? this.props.visible : this.state.visible;

const backTopBtn = visible ? (
<div {...divProps} className={classString} onClick={this.scrollToTop}>
{children || defaultElement}
</div>
) : null;

return (
<Animate component="" transitionName="fade">
{backTopBtn}
</Animate>
);
};

render() {
return <ConfigConsumer>{this.renderBackTop}</ConfigConsumer>;
}
}

先分離一下關注點。這一段的代碼是執行事件的注冊和移除,相應到 Hook 的方法就是useEffectuseEffect可以返回函數,該函數將在unmount時被調用。

1
2
3
4
5
6
7
8
9
10
11
componentDidMount() {
const getTarget = this.props.target || getDefaultTarget;
this.scrollEvent = addEventListener(getTarget(), 'scroll', this.handleScroll);
this.handleScroll();
}

componentWillUnmount() {
if (this.scrollEvent) {
this.scrollEvent.remove();
}
}

轉換後

1
2
3
4
5
6
7
8
9
10
useEffect(() => {
const getTarget = this.props.target || getDefaultTarget;
const scrollEvent = addEventListener(getTarget(), "scroll", handleScroll); // 先假設有這個方法
handleScroll();
return () => {
if (scrollEvent) {
scrollEvent.remove();
}
};
});

然後是這個很明顯的constructor,是用useStateuseState返回一個數組,第一個元素就是state,第二個元素就是改變狀態的函數,具體可以在數組解構的時候重新命名,一般的命名習慣是xxx和setXxx,useState的實踐方法也分為兩種,一種是每一個狀態的調用一次useState,得到一組的xxx和setXxx,另一種作法是把所有狀態放在一個,只有一個state和setState。

1
2
3
4
5
6
constructor(props: BackTopProps) {
super(props);
this.state = {
visible: false,
};
}

轉換後

1
const [visible, setVisible] = useState(false);

其它的就沒有甚麼特別了,就照抄一遍就行

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

const BackTop: FunctionComponent<BackTopProps> = (props: BackTopProps) => {
const [visible, setVisible] = useState(false);
const handleScroll = () => {
const { visibilityHeight, target = getDefaultTarget } = this.props;
const scrollTop = getScroll(target(), true);
setVisible(
visible: scrollTop > (visibilityHeight as number),
);
};

useEffect(() => {
const getTarget = this.props.target || getDefaultTarget;
const scrollEvent = addEventListener(getTarget(), "scroll", handleScroll);
handleScroll();
return () => {
if (scrollEvent) {
scrollEvent.remove();
}
};

const scrollToTop = (e: React.MouseEvent<HTMLDivElement>) => {
const { target = getDefaultTarget, onClick } = this.props;
scrollTo(0, {
getContainer: target
});
if (typeof onClick === "function") {
onClick(e);
}
};

const renderBackTop = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
className = "",
children
} = this.props;
const prefixCls = getPrefixCls("back-top", customizePrefixCls);
const classString = classNames(prefixCls, className, {
[`${prefixCls}-rtl`]: direction === "rtl"
});

const defaultElement = (
<div className={`${prefixCls}-content`}>
<div className={`${prefixCls}-icon`} />
</div>
);

const divProps = omit(this.props, [
"prefixCls",
"className",
"children",
"visibilityHeight",
"target",
"visible"
]);

const visible =
"visible" in this.props ? this.props.visible : this.state.visible;

const backTopBtn = visible ? (
<div {...divProps} className={classString} onClick={this.scrollToTop}>
{children || defaultElement}
</div>
) : null;

return (
<Animate component="" transitionName="fade">
{backTopBtn}
</Animate>
);
};

return <ConfigConsumer>{this.renderBackTop}</ConfigConsumer>;
});

}
# React
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×