前言
在 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; }
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 的方法就是useEffect
,useEffect
可以返回函數,該函數將在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,是用useState
,useState
返回一個數組,第一個元素就是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>; });
}
|