不要在循环,条件或嵌套函数中调用 Hook, 确保总是在React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。
为什么要保证顺序?
因为React通过Hook调用的顺序来确定state和useState的对应关系
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;
});
// ...
}
// ------------
// 首次渲染
// ------------
useState('Mary') // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm) // 2. 添加 effect 以保存 form 操作
useState('Poppins') // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle) // 4. 添加 effect 以更新标题
// -------------
// 二次渲染
// -------------
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm) // 2. 替换保存 form 的 effect
useState('Poppins') // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle) // 4. 替换更新标题的 effect
// ...
只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
在第一次渲染中 name !== ‘’ 这个条件值为 true,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm) // 此 Hook 被忽略!
useState('Poppins') // 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle) // 3 (之前为 4)。替换更新标题的 effect 失败
React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 persistForm 的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。
1.不要在普通的 JavaScript 函数中调用 Hook
2.在 React 的函数组件中调用 Hook
3.在自定义 Hook 中调用其他 Hook
作用:返回一个 state,以及更新 state 的函数。setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
使用:在渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。
const [state, setState] = useState(initialState);
setState(newState);
注意:
1.如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。
2.initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。
3.如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state。
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
4.如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。
下面的计数器组件示例展示了 setState 的两种用法:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
useState 目前的一种实践,是将变量名打平,而非像 Class Component 一样写在一个 State 对象里:
class ClassComponent extends React.PureComponent {
state = {
left: 0,
top: 0,
width: 100,
height: 100
};
}
function FunctionComponent {
const [left,setLeft] = useState(0)
const [top,setTop] = useState(0)
const [width,setWidth] = useState(100)
const [height,setHeight] = useState(100)
}
实际上在 Function Component 中也可以聚合管理 State:
function FunctionComponent() {
const [state, setState] = useState({
left: 0,
top: 0,
width: 100,
height: 100
});
}
只是更新的时候,不再会自动 merge,而需要使用 …state 语法:
setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
性能注意事项
useState 函数的参数虽然是初始值,但由于整个函数都是 Render,因此每次初始化都会被调用,如果初始值计算非常消耗时间,建议使用函数传入,这样只会执行一次:
function FunctionComponent(props) {
const [rows, setRows] = useState(() => createRows(props.count));
}
作用:该 Hook 接收一个包含命令式、且可能有副作用代码的函数。
副作用:在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作。
执行时机:effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候才执行。
每次Render的时候,里面的变量都是全新的(所以用const定义state变量是无伤大雅的,因为根本就不需要在同一次render中改变state)。
其实不仅是对象,函数在每次渲染时也是独立的。这就是 Capture Value 特性,后面遇到这种情况就不会一一展开,只描述为 “此处拥有 Capture Value 特性”。
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
解释了为什么下面的代码会输出 5 而不是 3:
const App = () => {
const [temp, setTemp] = React.useState(5);
const log = () => {
setTimeout(() => {
console.log("3 秒前 temp = 5,现在 temp =", temp);
}, 3000);
};
return (
<div
onClick={() => {
log();
setTemp(3);
// 3 秒前 temp = 5,现在 temp = 5
}}
>
xyz
</div>
);
};
在 log 函数执行的那个 Render 过程里,temp 的值可以看作常量 5,执行 setTemp(3) 时会交由一个全新的 Render 渲染,所以不会执行 log 函数。而 3 秒后执行的内容是由 temp 为 5 的那个 Render 发出的,所以结果自然为5
原因就是 temp、log 都拥有 Capture Value 特性。
利用 useRef 就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
// ...
}
也可以简洁的认为,ref 是 Mutable(可变的) 的,而 state 是 Immutable(不可变的) 的。
在组件被销毁时,通过 useEffect 注册的监听需要被销毁,这一点可以通过 useEffect 的返回值做到:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
Function Component 不存在生命周期,所以不要把 Class Component 的生命周期概念搬过来试图对号入座。Function Component 仅描述 UI 状态,React 会将其同步到 DOM,仅此而已。
既然是状态同步,那么每次渲染的状态都会固化下来,这包括 state props useEffect 以及写在 Function Component 中的所有函数。
然而舍弃了生命周期的同步会带来一些性能问题,所以我们需要告诉 React 如何比对 Effect。
告诉 React 如何对比 Effects
虽然 React 在 DOM 渲染时会 diff 内容,只对改变部分进行修改,而不是整体替换,但却做不到对 Effect 的增量修改识别。因此需要开发者通过 useEffect 的第二个参数告诉 React 用到了哪些外部变量:
import React, { useState, useEffect } from 'react'
function Lianxi() {
const [namea, setNamea] = useState(1);
const [nameb, setNameb]=useState('lishuang')
function ona(){
setNamea(namea+1)
}
useEffect(() => {
console.log('fsgh')
}, [nameb]); // Wrong: name is missing in dep
function onb(){
setNameb(nameb+'1')
}
return (
<div>
<button onClick={ona}>{namea}</button>
<button onClick={onb}>{nameb}</button>
</div>
)
}
export default Lianxi
直到 nameb 改变时的 Render,useEffect 才会再次执行,namea改变时的Render,useEffect不执行
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
<button onClick={()=>{setCount(count+1)}}>{count}</button>
这样每一次点击button,count都是从0开始的,因为每一次render的时候setInterval还没执行
setInterval 我们只想执行一次,所以我们自以为聪明的向 React 撒了谎,将依赖写成 []。
“组件初始化执行一次 setInterval,销毁时执行一次 clearInterval,这样的代码符合预期。” 你心里可能这么想。
但是你错了,由于 useEffect 符合 Capture Value 的特性,拿到的 count 值永远是初始化的 0。相当于 setInterval 永远在 count 为 0 的 Scope 中执行,你后续的 setCount 操作并不会产生任何作用。
笔者稍稍修改了一下标题,因为诚实是要付出代价的:
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
你老实告诉 React “嘿,等 count 变化后再执行吧”,那么你会得到一个好消息和两个坏消息。
好消息是,代码可以正常运行了,拿到了最新的 count。
坏消息有:
计时器不准了,因为每次 count 变化时都会销毁并重新计时。
频繁 生成/销毁 定时器带来了一定性能负担。
上述例子使用了 count,然而这样的代码很别扭,因为你在一个只想执行一次的 Effect 里依赖了外部变量。
既然要诚实,那只好 想办法不依赖外部变量:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
setCount 还有一种函数回调模式,你不需要关心当前值是什么,只要对 “旧的值” 进行修改即可。这样虽然代码永远运行在第一次 Render 中,但总是可以访问到最新的 state。
返回一个缓存的函数
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
返回一个缓存的变量
作用:有助于避免在每次渲染时都进行高开销的计算。(相当于vue的计算属性,把变量给缓存起来了)
执行时机:把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。如果传入一个空数组[]会只执行一次,如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
注意:传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
用 useMemo 做局部 PureRender
const Child = (props) => {
useEffect(() => {
props.fetchData()
}, [props.fetchData])
return useMemo(() => (
// ...
), [props.fetchData])
}
可以看到,我们利用 useMemo 包裹渲染代码,这样即便函数 Child 因为 props 的变化重新执行了,只要渲染函数用到的 props.fetchData 没有变,就不会重新渲染。
这里发现了 useMemo 的第一个好处:更细粒度的优化渲染。所谓更细粒度的优化渲染,是指函数 Child 整体可能用到了 A、B 两个 props,而渲染仅用到了 B,使用 useMemo的话,A改变不会导致重新渲染。
作用:useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。(可以简单理解为:通过 useRef 创建的对象,其值只有一份,而且在所有 Rerender 之间共享。与useState的区别在于 useState创建的对象,在每个Rerender内都是独立的而ueseRef是共享的)
function Counter() {
const count = useRef(0);
const log = () => {
count.current++;
setTimeout(() => {
console.log(count.current);
}, 3000);
};
return (
<div>
<p>You clicked {count.current} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
先介绍一下 useReducer 的用法:
const [state, dispatch] = useReducer(reducer, initialState);
useReducer 返回的结构与 useState 很像,只是数组第二项是 dispatch,而接收的参数也有两个,初始值放在第二位,第一位就是 reducer。
reducer 定义了如何对数据进行变换,比如一个简单的 reducer 如下:
function reducer(state, action) {
switch (action.type) {
case "increment":
return {
...state,
count: state.count + 1
};
default:
return state;
}
}
这样就可以通过调用 dispatch({ type: ‘increment’ }) 的方式实现 count 自增了。
作用:当数据嵌套的太深或者太复杂的时候可以用useImmer
const [state, setState] = useImmer({
people: [
{
name: '马云',
englishName: 'Jack Ma'
},
{
name: '马化腾',
englishName: 'Pony Ma'
},
{
name: '李彦宏',
englishName: 'Robin Li'
}
]
})
setState(state => {state.people[2].name = 'Robin Lee'})