大家好,我是 CUGGZ。
最初,在 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 (); }}export default ManagingStateClass;Count: {this.state.counter}
下面来使用 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 ();};export default ManagingStateHooks;Count: {counter}
该组件是一个返回 JSX 的函数,使用useStatehook来管理计算器的状态。它返回一个包含两个值的数组:第一个值为状态,第二个值为更新函数。并且使用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 (); }}export default StateChangesClass;Count: {this.state.counter}
当状态发生变化时,我们将新的计数器值保存在localStorage中。在函数组件中,我们可以通过使用useEffecthook 来实现相同的功能。
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 ();};export default StateChangesHooks;Count: {counter}
这个useEffecthook 有两个参数,第一个参数是回调函数,第二个参数是依赖数组。在组件挂载时,这个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 ({this.state.posts.map(post => { return); }}export default FetchingDataClass{post.title}; })}
有了 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 (4. 卸载组件时清理副作用{posts.map(post => { return);};export default FetchingDataHooks;{post.title}; })}
在卸载组件时清理副作用是非常重要的,否则可能会导致内存泄露。例如,在一个组件中,我们想要监听一个事件,比如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 (Window: {this.state.width} x {this.state.height}); }}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 (5. 防止组件重新渲染Window: {this.state.width} x {this.state.height});};export default CleanupHooks;
React 非常快,通常我们不必担心过早的优化。但是,在某些情况下,优化组件并确保它们不会过于频繁地重新渲染是很有必要的。
例如,减少类组件重新渲染的常用方法是使用PureComponent或者shouldComponentUpdate生命周期。下面例子中有两个类组件(父组件和子组件),父组件有两个状态值:counter和fruit。子组件只在父组件的fruit发生变化时重新渲染。所以,使用shouldComponentUpdate 生命周期来检查fruit属性是否改变。如果相同,则子组件不会重新渲染。
父组件:
import { Component } from "react";import PreventRerenderClass from "./PreventRerenderClass.jsx";function randomInteger(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min;}const fruits = ["banana", "orange", "apple", "kiwi", "mango"];class PreventRerenderExample extends Component { state = { fruit: null, counter: 0, }; pickFruit = () => { const fruitIdx = randomInteger(0, fruits.length - 1); const nextFruit = fruits[fruitIdx]; this.setState({ fruit: nextFruit, }); }; componentDidMount() { this.pickFruit(); } render() { return (); }}export default PreventRerenderExample;Current fruit: {this.state.fruit} | counter: {this.state.counter}
子组件:
import { Component } from "react";class PreventRerenderClass extends Component { shouldComponentUpdate(nextProps, nextState) { return this.props.fruit !== nextProps.fruit; } render() { return (); }}export default PreventRerenderClass;Fruit: {this.props.fruit}
随着 hooks 的引入,我们得到了一个新的高阶组件,称为memo。它可用于优化性能并防止函数组件重新渲染。下面来看看它是怎么用的。
父组件:
import { useState, useEffect } from "react";import PreventRerenderHooks from "./PreventRerenderHooks.jsx";function randomInteger(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min;}const fruits = ["banana", "orange", "apple", "kiwi", "mango"];const PreventRerenderExample = () => { const [fruit, setFruit] = useState(null); const [counter, setCounter] = useState(0); const pickFruit = () => { const fruitIdx = randomInteger(0, fruits.length - 1); const nextFruit = fruits[fruitIdx]; setFruit(nextFruit); }; useEffect(() => { pickFruit(); }, []); return ();};export default PreventRerenderExample;Current fruit: {fruit} | counter: {counter}
子组件:
import { memo } from "react";const PreventRerenderHooks = props => { return ();};export default memo(PreventRerenderHooks);Fruit: {props.fruit}
PreventRerenderHooks组件使用memo组件包装,并且仅在 props 中的 fruit 发生变化时发挥重新渲染。需要注意,memo组件执行的是浅比较,因此如果需要更好地控制memo组件何时重新渲染,可以提供自己的函数来执行props比较。
import { memo } from "react";const PreventRerenderHooks = props => { return (6. Context API);};export default memo(PreventRerenderHooks, (prevProps, nextProps) => { return prevProps.fruit !== nextProps.fruit});Fruit: {props.fruit}
Context API 是一个很好用的工具,可以为组件层次结构中不同级别的组件提供值。可以使用 React 提供的createContext方法创建新的上下文。先来看一个在类组件中使用context的例子。
Context Provider:
import { createContext } from "react";export const UserContext = createContext();export const UserActionsContext = createContext();
在父组件中,向消费者提供了UserContext和UserActionsContext。
import { Component, createContext } from "react";import ContextApiClassConsumer from "./ContextApiClassConsumer.jsx";import { UserContext, UserActionsContext } from "./userContext.js";class ContextApiHooksProvider extends Component { state = { user: { name: "Class", }, }; setUser = user => this.setState({ user }); render() { return (); }}export default ContextApiHooksProvider;
这里ContextApiClassConsumer组件就可以获取到父组件提供的user和setUser。
Context Consumer:
import { Component } from "react";import { UserContext, UserActionsContext } from "./userContext.js";class ContextApiClassConsumer extends Component { render() { return ({user => ( ); }}export default ContextApiClassConsumer;{setUser => ( )}setUser({ name: e.target.value, }) } />)}
在上面的例子中,UserContext.Consumer组件的子函数接收 user 状态,UserActionsContext.Consumer的子函数接收setUser方法。
使用 Hooks 实现和上面的代码非常类似,但是会更简洁。同样,我们使用UserContext.Provider和UserActionsContext.Provider组件来提供user状态和setUser方法。
Context Provider:
import { useState } from "react";import ContextApiHooksConsumer from "./ContextApiHooksConsumer.jsx";import { UserContext, UserActionsContext } from "./userContext.js";const ContextApiHooksProvider = () => { const [user, setUser] = useState({ name: "Hooks", }); return ();};export default ContextApiHooksProvider;
在函数组件中,我们可以像在类组件中一样使用context,但是,hooks 中有一种更简洁的方法,我们可以利用useContexthook 来访问context值。
Context Consumer:
import { useContext } from "react";import { UserContext, UserActionsContext } from "./userContext.js";const ContextApiHooksConsumer = () => { const user = useContext(UserContext); const setUser = useContext(UserActionsContext); return (7. 跨重新渲染保留值setUser({ name: e.target.value, }) } />);};export default ContextApiHooksConsumer;
在某些情况下,我们可能需要再组件中存储一些数据。但是不希望将其存储在状态中,因为 UI 不以任何方式依赖这些数据。
例如,我们可能会保存一些希望稍后包含在 API 请求中的元数据。这在类组件中很容易实现,只需为类分配一个新属性即可。
import { Component } from "react";class PreservingValuesClass extends Component { state = { counter: 0, }; componentDidMount() { this.valueToPreserve = Math.random(); } showValue = () => { alert(this.valueToPreserve); }; increment = () => this.setState(({ counter }) => ({ counter: counter + 1 })); render() { return (); }}export default PreservingValuesClass;Counter: {this.state.counter}
在这个例子中,当组件被挂载时,我们在valueToPreserve属性上分配了一个动态随机数。除此之外,还有 increment 方法来强制重新渲染,但是Show按钮时会弹窗显示保留的值。
这在类组件中很容易实现,但是在函数组件中就没那么简单了。这是因为,任何时候函数组件的重新渲染都会导致函数中的所有内容重新执行。这意味着如果我们有这样的组件:
const MyComponent = props => { const valueToPreserve = Math.random() // ...}
组件每次重新渲染时都会重新调用Math.random()方法,因此创建的第一个值将丢失。
避免此问题的一种方法是将变量移到组件之外。但是,这是行不通的,因为如果该组件被多次使用,则该值会将被它们中的每一个覆盖。
恰好,React 提供了一个非常适合这个用例的 hook。我们可以通过使用useRefhook 来保留函数组件中重新渲染的值。
import { useState, useRef, useEffect } from "react";const PreserveValuesHooks = props => { const valueToPreserve = useRef(null); const [counter, setCounter] = useState(0); const increment = () => setCounter(counter => counter + 1); const showValue = () => { alert(valueToPreserve.current); }; useEffect(() => { valueToPreserve.current = Math.random(); }, []); return ();};export default PreserveValuesHooks;Counter: {counter}
valueToPreserve 是一个初始值为null的ref。但是,它后来在useEffect中更改为我们想要保留的随机数。
8. 如何向父组件传递状态和方法?尽管我们不应该经常访问子组件的状态和属性,但是在某些情况下它可能会很有用。例如,我们想要重置某些组件的状态或者访问它的状态。我们需要创建一个 Ref,可以在其中存储对想要访问的子组件的引用。在类组件中,可以使用createRef方法,然后将该ref传递给子组件。
父组件:
import { Component, createRef } from "react";import ExposePropertiesClassChild from "./ExposePropertiessClassChild";class ExposePropertiesClassParent extends Component { constructor(props) { super(props); this.childRef = createRef(); } showValues = () => { const counter = this.childRef.current.state.counter; const multipliedCounter = this.childRef.current.getMultipliedCounter(); alert(` counter: ${counter} multipliedCounter: ${multipliedCounter} `); }; increment = () => this.setState(({ counter }) => ({ counter: counter + 1 })); render() { return (); }}export default ExposePropertiesClassParent;
子组件:
import { Component } from "react";class ExposePropertiesClassChild extends Component { state = { counter: 0, }; getMultipliedCounter = () => { return this.state.counter * 2; }; increment = () => this.setState(({ counter }) => ({ counter: counter + 1 })); render() { return (); }}export default ExposePropertiesClassChild;Counter: {this.state.counter}
要访问子组件的属性,只需要在父组件中创建一个ref并传递它。现在,让我们看看如何使用函数组件和 hook 来实现相同的目标。
父组件:
import { useRef } from "react";import ExposePropertiesHooksChild from "./ExposePropertiesHooksChild";const ExposePropertiesHooksParent = props => { const childRef = useRef(null); const showValues = () => { const counter = childRef.current.counter; const multipliedCounter = childRef.current.getMultipliedCounter(); alert(` counter: ${counter} multipliedCounter: ${multipliedCounter} `); }; return ();};export default ExposePropertiesHooksParent;
在父组件中,我们使用useRefhook 来存储对子组件的引用。然后在 showValues 函数中访问childRef的值。可以看到,这里与类组件中的实现非常相似。
子组件:
import { useState, useImperativeHandle, forwardRef } from "react";const ExposePropertiesHooksChild = (props, ref) => { const [counter, setCounter] = useState(0); const increment = () => setCounter(counter => counter + 1); useImperativeHandle(ref, () => { return { counter, getMultipliedCounter: () => counter * 2, }; }); return ();};export default forwardRef(ExposePropertiesHooksChild);Counter: {counter}
forwardRef将从父组件传递的ref转发到组件,而useImperativeHandle指定了父组件应该可以访问的内容。
9. 小结通过这篇文章,相信你对使用Hooks(函数组件)来重构类组件有了一定了解。Hooks 的出现使得 React 代码更加简洁,并且带来了更好的状态逻辑可重用性。在开始编写 Hooks 之前,建议先阅读 React Hooks 的官方文档,因为在编写时需要遵循某些规则,例如不要改变 Hooks 的调用顺序。