如何使用 React Hooks 重构类组件?(上)https://developer.aliyun.com/article/1411375
5. 防止组件重新渲染
React 非常快,通常我们不必担心过早的优化。但是,在某些情况下,优化组件并确保它们不会过于频繁地重新渲染是很有必要的。
例如,减少类组件重新渲染的常用方法是使用 PureComponent
或者 shouldComponentUpdate
生命周期。下面例子中有两个类组件(父组件和子组件),父组件有两个状态值:counter
和 fruit
。子组件只在父组件的 fruit
发生变化时重新渲染。所以,使用 shouldComponentUpdat
e 生命周期来检查 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 ( <div> <h3> Current fruit: {this.state.fruit} | counter: {this.state.counter} </h3> <button onClick={this.pickFruit}>挑一个水果</button> <button onClick={() => this.setState(({ counter }) => ({ counter: counter + 1, })) } > Increment </button> <button onClick={() => this.setState(({ counter }) => ({ counter: counter - 1 })) } > Decrement </button> <div className="section"> <PreventRerenderClass fruit={this.state.fruit} /> </div> </div> ); } } export default PreventRerenderExample;
子组件:
import { Component } from "react"; class PreventRerenderClass extends Component { shouldComponentUpdate(nextProps, nextState) { return this.props.fruit !== nextProps.fruit; } render() { return ( <div> <p>Fruit: {this.props.fruit}</p> </div> ); } } export default PreventRerenderClass;
随着 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 ( <div> <h3> Current fruit: {fruit} | counter: {counter} </h3> <button onClick={pickFruit}>挑一个水果</button> <button onClick={() => setCounter(counter => counter + 1)}> Increment </button> <button onClick={() => setCounter(counter => counter - 1)}> Decrement </button> <div className="section"> <PreventRerenderHooks fruit={fruit} /> </div> </div> ); }; export default PreventRerenderExample;
子组件:
import { memo } from "react"; const PreventRerenderHooks = props => { return ( <div> <p>Fruit: {props.fruit}</p> </div> ); }; export default memo(PreventRerenderHooks);
PreventRerenderHooks
组件使用 memo
组件包装,并且仅在 props 中的 fruit 发生变化时发挥重新渲染。需要注意,memo组件执行的是浅比较,因此如果需要更好地控制memo
组件何时重新渲染,可以提供自己的函数来执行 props
比较。
import { memo } from "react"; const PreventRerenderHooks = props => { return ( <div> <p>Fruit: {props.fruit}</p> </div> ); }; export default memo(PreventRerenderHooks, (prevProps, nextProps) => { return prevProps.fruit !== nextProps.fruit });
6. Context API
Context API 是一个很好用的工具,可以为组件层次结构中不同级别的组件提供值。 可以使用 React 提供的 createContext
方法创建新的上下文。先来看一个在类组件中使用 context
的例子。
Context Provider:
import { createContext } from "react"; export const UserContext = createContext(); export const UserActionsContext = createContext();
在父组件中,向消费者提供了 UserContext
和 UserActionsContext
。
typescript
复制代码
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 ( <UserContext.Provider value={this.state.user}> <UserActionsContext.Provider value={this.setUser}> <ContextApiClassConsumer /> </UserActionsContext.Provider> </UserContext.Provider> ); } } export default ContextApiHooksProvider;
这里 ContextApiClassConsumer
组件就可以获取到父组件提供的user
和setUser
。
Context Consumer:
import { Component } from "react"; import { UserContext, UserActionsContext } from "./userContext.js"; class ContextApiClassConsumer extends Component { render() { return ( <UserContext.Consumer> {user => ( <UserActionsContext.Consumer> {setUser => ( <div> <input type="text" value={user.name} onChange={e => setUser({ name: e.target.value, }) } /> </div> )} </UserActionsContext.Consumer> )} </UserContext.Consumer> ); } } export default ContextApiClassConsumer;
在上面的例子中,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 ( <UserContext.Provider value={user}> <UserActionsContext.Provider value={setUser}> <ContextApiHooksConsumer /> </UserActionsContext.Provider> </UserContext.Provider> ); }; export default ContextApiHooksProvider;
在函数组件中,我们可以像在类组件中一样使用 context
,但是,hooks 中有一种更简洁的方法,我们可以利用 useContext
hook 来访问 context
值。
Context Consumer:
import { useContext } from "react"; import { UserContext, UserActionsContext } from "./userContext.js"; const ContextApiHooksConsumer = () => { const user = useContext(UserContext); const setUser = useContext(UserActionsContext); return ( <div> <input type="text" value={user.name} onChange={e => setUser({ name: e.target.value, }) } /> </div> ); }; export default ContextApiHooksConsumer;
7. 跨重新渲染保留值
在某些情况下,我们可能需要再组件中存储一些数据。但是不希望将其存储在状态中,因为 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 ( <div> <p>Counter: {this.state.counter}</p> <button onClick={this.increment}>Increment</button> <button onClick={this.showValue}>Show</button> </div> ); } } export default PreservingValuesClass;
在这个例子中,当组件被挂载时,我们在 valueToPreserve
属性上分配了一个动态随机数。除此之外,还有 increment 方法来强制重新渲染,但是Show
按钮时会弹窗显示保留的值。
这在类组件中很容易实现,但是在函数组件中就没那么简单了。这是因为,任何时候函数组件的重新渲染都会导致函数中的所有内容重新执行。这意味着如果我们有这样的组件:
const MyComponent = props => { const valueToPreserve = Math.random() // ... }
组件每次重新渲染时都会重新调用 Math.random()
方法,因此创建的第一个值将丢失。
避免此问题的一种方法是将变量移到组件之外。 但是,这是行不通的,因为如果该组件被多次使用,则该值会将被它们中的每一个覆盖。
恰好,React 提供了一个非常适合这个用例的 hook。 我们可以通过使用 useRef
hook 来保留函数组件中重新渲染的值。
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 ( <div> <p>Counter: {counter}</p> <button onClick={increment}>Increment</button> <button onClick={showValue}>Show value</button> </div> ); }; export default PreserveValuesHooks;
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 ( <div> <button onClick={this.showValues}>Show</button> <ExposePropertiesClassChild ref={this.childRef} /> </div> ); } } 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 ( <div> <p>Counter: {this.state.counter}</p> <button onClick={this.increment}>Increment</button> </div> ); } } export default ExposePropertiesClassChild;
要访问子组件的属性,只需要在父组件中创建一个 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 ( <div> <button onClick={showValues}>Show child values</button> <ExposePropertiesHooksChild ref={childRef} /> </div> ); }; export default ExposePropertiesHooksParent;
在父组件中,我们使用 useRef
hook 来存储对子组件的引用。 然后在 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 ( <div> <p>Counter: {counter}</p> <button onClick={increment}>Increment</button> </div> ); }; export default forwardRef(ExposePropertiesHooksChild);
forwardRef
将从父组件传递的 ref
转发到组件,而 useImperativeHandle
指定了父组件应该可以访问的内容。
9. 小结
通过这篇文章,相信你对使用Hooks(函数组件)来重构类组件有了一定了解。Hooks 的出现使得 React 代码更加简洁,并且带来了更好的状态逻辑可重用性。在开始编写 Hooks 之前,建议先阅读 React Hooks 的官方文档,因为在编写时需要遵循某些规则,例如不要改变 hooks 的调用顺序。