React Hooks
Hooks 的目的是加强函数组件,完全不使用"类"组件,就能写出一个全功能的组件, A Hook is a special function that lets you “hook into” React features.根据官网的说法,
Hook
就是一个特殊的函数,让你可以在函数组件中使用一些Hooks(钩子)
类组件的缺点
- 大型组件很难拆分和重构,也很难测试。
- 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
- 组件类引入了复杂的编程模式,比如 render props 和高阶组件。
React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。 React Hooks 就是那些钩子。
四个常用的钩子
- useState() 状态钩子
- useContext() 共享状态钩子
- useReducer() action 钩子
- useEffect() 副作用钩子
useState() 状态钩子
用于为函数组件引入状态 state,纯函数不能有状态,因此需要通过钩子来引入
import React, { useState } from "react";
function Example() {
// Declare a new state variable, which we'll call "count"
// useState 返回一个数组,通过解构的方式来获取对应的值
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
useState()这个函数接受状态的初始值,作为参数,上例的初始值为count
的初始值。该函数返回一个数组,数组的第一个成员是一个变量(上例是 count),指向状态的当前值。第二个成员是一个函数,用来更新状态,约定是 set 前缀加上状态的变量名(上例是 setCount)
如果我们想要存储两个不同的值在 state 中,那么我们可以调用两次 useState()
const [count, setCount] = useState(0);
const [isLoading, setLoading] = useState(true);
....
useContext() 共享状态钩子
如果需要在组件之间共享状态,可以使用 useContext()
const value = useContext(myContext);
复制代码 useContext 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。
别忘记 useContext 的参数必须是 context 对象本身:
正确: useContext(MyContext)
错误: useContext(MyContext.Consumer)
错误: useContext(MyContext.Provider)
现在有两个组件 Navbar 和 Messages,我们希望它们之间共享状态。
<div className="App">
<Navbar />
<Messages />
</div>
使用 React Context API 在组件外创建一个 Context
const AppContext = React.createContext({});
// 如果组件是单独的文件,那么需要在这里把AppContext导出
// export const AppContext = React.createContext({})
组件封装代码如下
<AppContext.Provider
value={{
username: "superawesome"
}}
>
<div className="App">
<Navbar />
<Messages />
</div>
</AppContext.Provider>
// 在需要的地方导入AppContext
import { AppContext } from "../index";
export default function Navbar() {
// console.log(useContext())
const { username } = useContext(AppContext);
return (
<div className="navbar">
<p>Navbar</p>
<p>{username}</p>
</div>
);
}
useReducer() action 例子
React 本身不提供状态管理的功能,通常会用到 Redux
Redux 的核心概念是,组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 Reducer 函数算出新的状态,Reducer 函数的形式是(state, action) => newState。
useReducers()钩子用来引入 Reducer 功能。
const [state, dispatch] = useReducer(reducer, initialState);
上面是 useReducer()的基本用法,它接受 Reducer 函数和状态的初始值作为参数,返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的 dispatch 函数。
import React, { useReducer } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const myReducer = (state, action) => {
switch (action.type) {
case "countUp":
return {
...state,
count: state.count + 1
};
default:
return state;
}
};
function App() {
const [state, dispatch] = useReducer(myReducer, { count: 0 });
return (
<div className="App">
<button onClick={() => dispatch({ type: "countUp" })}>+1</button>
<p>Count: {state.count}</p>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
由于 Hooks 可以提供共享状态和 Reducer 函数,所以它在这些方面可以取代 Redux。但是,它没法提供中间件(middleware)和时间旅行(time travel),如果你需要这两个功能,还是要用 Redux。
useEffect() 副作用钩子
useEffect()用来引入具有副作用的操作,最常见的就是向服务器请求数据。以前,放在 componentDidMount 里面的代码,现在可以放在 useEffect()。
useEffect()的用法如下。
useEffect(() => {
// Async Action
}, [dependencies]);
上面用法中,useEffect()接受两个参数。第一个参数是一个函数,异步操作的代码放在里面。第二个参数是一个数组,用于给出 Effect 的依赖项(简单理解就是异步请求需要的参数),只要这个数组发生变化,useEffect()就会执行。第二个参数可以省略,这时每次组件渲染时,就会执行 useEffect()。
Tip:
useEffect
可以理解为componentDidMount
componentDidUpdate
componentWillUnMount
三个生命钩子函数的结合
const Person = ({ personId }) => {
const [loading, setLoading] = useState(true);
const [person, setPerson] = useState({});
useEffect(() => {
setLoading(true);
fetch(`https://swapi.co/api/people/${personId}/`)
.then(response => response.json())
.then(data => {
setPerson(data);
setLoading(false);
});
}, [personId]);
if (loading === true) {
return <p>Loading ...</p>;
}
return (
<div>
<p>You're viewing: {person.name}</p>
<p>Height: {person.height}</p>
<p>Mass: {person.mass}</p>
</div>
);
};
每当组件参数 personId 发生变化,useEffect()就会执行。组件第一次渲染时,useEffect()也会执行。
effect 可以分为两类:一类是不需要清除的,比如:网络请求、手动修改 DOM、日志打印,还有一类是需要清除的,比如:监听事件。
不需要清除的对比 Class 组件,其实只是使用了 componentDidMount
componentDidUpdate
这两个生命钩子,但是使用了 useEffect 后,不用重复些一些代码在这两个函数中
需要清除的对比 Class 组件,使用了 componentDidMount
componentDidUpdate
componentWillUnMount
三个生命钩子函数,需要在卸载钩子函数中,把一些 Effect 进行清除,比如移除监听事件等。
在 Hook 中我们不需要单独的在某个钩子函数中添加清除的操作(并且函数组件中本来就没有生命周期钩子函数),在 useEffect 中我们可以通过返回一个函数的方式来实现清除的操作
当需要清理时,React 将运行它
// 这是官网上的一个例子
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
卸载组件时,React 执行清理。但是,正如我们之前所了解的,效果会在每个渲染中运行,而不仅仅是一次。这就是为什么 React 在下一次运行效果之前还要清除先前渲染中的效果的原因。
useEffect 同样可是使用多次,相比于 Class 组件中把同一个逻辑分割到各个生命周期中,在一个 useEffect 中就是一个完整的逻辑,使用多个 useEffect 就是多个逻辑
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
useEffect 的第二个参数,我们在实际的场景中会有这样一个情况,就是在重新渲染的时候,如果某些值没有改变那么我们就让 React 跳过应用某些效果。
class 中
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
useEffect 中,我们可以传递一个数组作为 useEffect 的第二个参数,意思是只有当这个参数的值发生改变的时候才会触发 effect
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
Tips:
- 如果使用此优化,请确保数组包含组件范围中所有随时间变化并被效果使用的值(例如 props 和 state)。
- 如果要运行效果并仅将其清除一次(在挂载和卸载时),则可以传递空数组([])作为第二个参数。
Hooks 的使用规则
顶层调用 Hooks
不要在循环中、条件判断或者嵌套的函数中调用 Hooks,因为这样才能保证 hooks 每次的调用顺序是不变的,react 才能准确的找到对应的 hook
React 依赖于 Hook 的调用顺序。
function Form() {
// 1. Use the name state variable
const [name, setName] = useState("Mary");
// 2. Use an effect for persisting the form
useEffect(function persistForm() {
localStorage.setItem("formData", name);
});
// 3. Use the surname state variable
const [surname, setSurname] = useState("Poppins");
// 4. Use an effect for updating the title
useEffect(function updateTitle() {
document.title = name + " " + surname;
});
// ...
}
react 就会依照这如下的顺序找到对应的 hook
useState("Mary"); // 1. Initialize the name state variable with 'Mary'
useEffect(persistForm); // 2. Add an effect for persisting the form
useState("Poppins"); // 3. Initialize the surname state variable with 'Poppins'
useEffect(updateTitle); // 4. Add an effect for updating the title
如果我们把某个 hook 放在 if 条件中
if (name !== "") {
useEffect(function persistForm() {
localStorage.setItem("formData", name);
});
}
这样就会导致 hook 的顺序发生改变
useState("Mary"); // 1. Read the name state variable (argument is ignored)
// useEffect(persistForm) // 🔴 This Hook was skipped!
useState("Poppins"); // 🔴 2 (but was 3). Fail to read the surname state variable
useEffect(updateTitle); // 🔴 3 (but was 4). Fail to replace the effect
我们不能把某个 hook 放在条件中,但是我们可以在 hook 中添加条件判断等来决定是否执行
useEffect(function persistForm() {
// 👍 We're not breaking the first rule anymore
if (name !== "") {
localStorage.setItem("formData", name);
}
});
函数组件中调用 Hooks
- 不要在常规的 js 函数中调用 Hooks
- 可以在 React 函数组件中调用
- 可以在自定义的 Hooks 中调用
自定义 Hooks
自定义 Hooks 可以帮助我们解决一些代码逻辑的复用,之前我们了解的复用的方式有 render props 和 HOC ,自定义 Hooks 也可以。
自定义 hooks 也是 js 的一个函数,但是我们约定以use
开头(因为 hooks 的规则中,不能再常规的 js 函数中使用 hooks,因此这里必须以 use 开头,告诉 react 这是一个自定义的 hook),每次使用自定义挂钩时,其中的所有状态和效果都是完全隔离的。
Hooks API
参考文档 Hooks API Reference
-
基础 Hooks
- useState
- useEffect
- useContext
-
其他 Hooks
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue