什么是Hook
Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。
使用Hook的目的
1、在class中代码需要按照生命周期去划分代码,但是在hook中则是按照代码的用途去划分,hook避免把相关的逻辑拆分到不同的地方。
2、Hook 使你在无需修改组件结构的情况下复用状态逻辑。
3、class 也给目前的工具带来了一些问题。例如,class不能很好的压缩,并且会使热重载出现不稳定的情况。
使用Hook的规则
1、只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。因为React 靠的是 Hook 调用的顺序来知道哪个 state 对应哪个 useState。
2、只能在 React 的函数组件中调用 Hook或者自定义的 Hook 中使用。在class组件内不起作用。不要在其他 JavaScript 函数中调用。
useState
state 只在组件首次渲染的时候被创建。在下一次重新渲染时,useState 返回给我们当前的 state。
useState返回的是一个数组 const [fruit,setFruit]是一种数组解构的方式。
const [fruit, setFruit] = useState('banana');
// 等同于
var fruitStateVariable = useState('banana'); // 返回一个有两个元素的数组
var fruit = fruitStateVariable[0]; // 数组里的第一个值
var setFruit = fruitStateVariable[1]; // 数组里的第二个值
setFruit的时候,如果值是{},则每次都会render,即使每次都是{},也会render,但是如果数据类型是string等,则不会render。因为{}相当于一个新的对象,{} != {}。
使用单个还是多个 state 变量
state不进行合并而是覆盖。我们可以选择写一个自定义Hook来专门做合并这件事情,也可以选择把state切分成多个state变量。
切分成多个state使得后期把一些相关的逻辑抽取到一个自定义 Hook 变得容易。
const [position, setPosition] = useState({ left: 0, top: 0, width: 100, height: 100 });
// 把state切分成多个state
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });
useEffect
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
}
副作用
在 React 组件中执行过数据获取、订阅或者手动修改过DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。
Effect Hook 可以让你在函数组件中执行副作用操作。
执行时机
仅在组件挂载、组件更新和卸载时执行。可以理解为对应class组件生命周期的componentDidMount,componentDidUpdate 和 componentWillUnmount时执行。
为什么传给useEffect的是不同的函数
useEffect(() => {
document.title = `You clicked ${count} times`;
});
// 为什么不选择下面的方式?
let f1 = () => {
document.title = `You clicked ${count} times`;
};
useEffect(f1);
事实上这正是React可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。
useEffect是异步还是同步
使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。
与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。
虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。
为什么在组件内部调用 useEffect
将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
如何清除副作用
虽然这个例子不需要清除副作用,但是还是用跟这个例子进行举例。通过return一个函数进行清除副作用。
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
return ()=>{
console.log("此处执行清除副作用的代码")
}
});
}
何时清除副作用
像前面所说的,传给useEffect的是不同的函数,所以每一次在执行effect之前,React 会对上一个 effect 进行清除。
之所以在每一次执行effect之前清除上一个effect,是因为此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。
如何跳过 Effect 进行性能优化
有些情况是不需要每次渲染都去执行effect的,我们通过给它传第二个参数来控制何时进行渲染。
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
useEffect(() => {
document.title = `You clicked ${count} times`;
}, []); // 仅在组件挂载和卸载时执行 对应class组件的componentDidMount 和 componentWillUnmount执行
自定义Hook
有时候我们会想要在组件之间重用一些状态逻辑。 目前为止,有两种主流方案来解决这个问题:高阶组件和render props。自定义Hook可以让你在不增加组件的情况下达到同样的目的。
自定义 Hook 更像是一种约定而不是功能。useSomething 的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。
useContext
在类组件中使用context
const ThemeContext = React.createContext(themes.light);
class MyClass extends React.Component {
render() {
let value = this.context;// class中的获取context数据的方式
}
}
MyClass.contextType = ThemeContext;
在函数组件中使用context
const ThemeContext = React.createContext(themes.light);
function MyClass() {
const theme = useContext(ThemeContext);// 函数组件中
}
useReducer
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, {count:0});
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useRef
// 类组件
const refContainer = React.createRef();
// 函数组件
const refContainer = useRef(initialValue);
<input ref={refContainer} type="text" />
FAQ
为什么我会在我的函数中看到陈旧的 props 和 state
组件内部的任何函数,包括事件处理函数和 effect,都是从它被创建的那次渲染中被「看到」的。
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);// 此处count是在点击Show alert的时候的count
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
有类似 forceUpdate 的东西吗
可以考虑如下方式,但尽量避免。
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}