最初,在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;
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
。
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 的调用顺序。