最初,在 React 中可以使用 createClass
来创建组件,后来被类组件所取代。在 React 16.8版本中,新增的 Hooks 功能彻底改变了我们编写React程序的方式,因为使用 Hooks 可以编写更简洁、更清晰的代码,并为创建可重用的有状态逻辑提供了更好的模式。
许多公司和开发人员都放弃了类组件转而使用 Hooks。而许多旧的的React 项目仍然在使用类组件。更重要的是,在类组件中有 Error Boundaries,而函数组件中是无法使用 Error Boundaries 的。
本文就来通过一些常见示例看看如何使用 React Hooks 来重构类组件。
1. 管理和更新组件状态
状态管理是几乎所有 React 应用中最重要的部分,React 基于 state 和 props 渲染组件。每当它们发生变化时,组件就会重新渲染,并且 DOM 也会相应地更新。下面来看一个计数器的例子,它包含一个计数状态以及两个更新它的地方:
import { Component } from "react"; class ManagingStateClass extends Component { state = { counter: 0, }; increment = () => { this.setState(prevState => { return { counter: prevState.counter + 1, }; }); }; decrement = () => { this.setState(prevState => { return { counter: prevState.counter - 1, }; }); }; render() { return ( <div> <div>Count: {this.state.counter}</div> <div> <button onClick={this.increment}>Increment</button> <button onClick={this.decrement}>Decrement</button> </div> </div> ); } } export default ManagingStateClass;
下面来使用 Hooks 实现这个计数器组件:
import { useState } from "react"; const ManagingStateHooks = () => { const [counter, setCounter] = useState(0); const increment = () => setCounter(counter => counter + 1); const decrement = () => setCounter(counter => counter - 1); return ( <div> <div>Count: {counter}</div> <div> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> </div> </div> ); }; export default ManagingStateHooks;
该组件是一个返回 JSX 的函数,使用 useState
hook来管理计算器的状态。它返回一个包含两个值的数组:第一个值为状态,第二个值为更新函数。并且使用 setCounter
来更新程序的increment
和decrement
函数。
2. 状态更新后的操作
在某些情况下,我们可能需要在状态更新时执行某些操作。在类组件中,我们通常会在componentDidUpdate
生命周期中实现该操作。
import { Component } from "react"; class StateChangesClass extends Component { state = { counter: 0, }; componentDidUpdate(prevProps, prevState) { localStorage.setItem("counter", this.state.counter); } increment = () => { this.setState(prevState => { return { counter: prevState.counter + 1, }; }); }; decrement = () => { this.setState(prevState => { return { counter: prevState.counter - 1, }; }); }; render() { return ( <div> <div>Count: {this.state.counter}</div> <div> <button onClick={this.increment}>Increment</button> <button onClick={this.decrement}>Decrement</button> </div> </div> ); } } export default StateChangesClass;
当状态发生变化时,我们将新的计数器值保存在 localStorage
中。在函数组件中,我们可以通过使用 useEffect
hook 来实现相同的功能。
import { useState, useEffect } from "react"; const StateChangesHooks = () => { const [counter, setCounter] = useState(0); const increment = () => setCounter(counter => counter + 1); const decrement = () => setCounter(counter => counter - 1); useEffect(() => { localStorage.setItem("counter", counter); }, [counter]); return ( <div> <div>Count: {counter}</div> <div> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> </div> </div> ); }; export default StateChangesHooks;
这个 useEffect
hook 有两个参数,第一个参数是回调函数,第二个参数是依赖数组。在组件挂载时,这个 hook
至少会执行一次。然后,仅在依赖数组内的任何值发生变化时都会触发第一个参数传入的回调函数。如果依赖数组为空,则回调函数只会执行一次。在上面的例子中,每当 counter
发生变化时,都会触发将 counter
保存在 localStorage
中的回调函数。
3. 获取数据
在类组件中,通过会在componentDidMount生命周期中初始化一个 API 请求来获取数据。下面来看一个获取并显示帖子列表的组件:
import { Component } from "react"; class FetchingDataClass extends Component { state = { posts: [], }; componentDidMount() { this.fetchPosts(); } fetchPosts = async () => { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); this.setState({ posts: data.slice(0, 10), }); }; render() { return ( <div> {this.state.posts.map(post => { return <div key={post.id}>{post.title}</div>; })} </div> ); } } export default FetchingDataClass
有了 hooks,就可以使用useEffect
来实现上述功能。它会在第一次挂载之后执行一次,然后在任何依赖发生变化时再次触发。useEffect
允许我们传入一个空依赖数组作为第二个参数来确保只执行一次effect
的回调函数。
import { useState, useEffect } from "react"; const FetchingDataHooks = () => { const [posts, setPosts] = useState([]); const fetchPosts = async () => { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); setPosts(data.slice(0, 10)); }; useEffect(() => { fetchPosts(); }, []); return ( <div> {posts.map(post => { return <div key={post.id}>{post.title}</div>; })} </div> ); }; export default FetchingDataHooks;
4. 卸载组件时清理副作用
在卸载组件时清理副作用是非常重要的,否则可能会导致内存泄露。例如,在一个组件中,我们想要监听一个事件,比如resize
或者scroll
,并根据窗口大小获滚动的位置来做一些事情。下面来看一个类组件的例子,它会监听 resize
事件,然后更新浏览器窗口的宽度和高度的状态。事件监听器在 componentWillUnmount
生命周期中被移除。
import { Component } from "react"; class CleanupClass extends Component { state = { width: window.innerWidth, height: window.innerHeight, }; componentDidMount() { window.addEventListener("resize", this.updateWindowSize, { passive: true, }); } componentWillUnmount() { window.removeEventListener("resize", this.updateWindowSize, { passive: true, }); } updateWindowSize = () => { this.setState({ width: window.innerWidth, height: window.innerHeight, }); }; render() { return ( <div> Window: {this.state.width} x {this.state.height} </div> ); } } export default CleanupClass;
在 useEffect
中,我们可以在回调函数中返回一个函数来执行清理操作,卸载组件时会调用此函数。下面,首先来定义一个 updateWindowSize
函数,然后在 useEffect
中添加 resize
事件监听器。 接下来返回一个匿名箭头函数,它将用来移除监听器。
import { useState, useEffect } from "react"; const CleanupHooks = () => { const [width, setWidth] = useState(window.innerWidth); const [height, setHeight] = useState(window.innerHeight); useEffect(() => { const updateWindowSize = () => { setWidth(window.innerWidth); setHeight(window.innerHeight); }; window.addEventListener("resize", updateWindowSize, { passive: true, }); return () => { window.removeEventListener("resize", this.updateWindowSize, { passive: true, }); }; }, []); return ( <div> Window: {this.state.width} x {this.state.height} </div> ); }; export default CleanupHooks;
如何使用 React Hooks 重构类组件?(下)https://developer.aliyun.com/article/1411376