React Hook填坑(二)
如果已经使用过 Hook,相信你一定回不去了,这种用函数的方式去编写有状态组件简直太爽啦。
如果还没使用过 Hook,那你要赶紧升级你的 React(v16.8+),投入 Hook 的怀抱吧。
至于 Hook 的好处这里就不多说了,上一篇已经讲过了——React Hook上车(一)。
Hook 虽好,操作不当可是容易翻车的哦。
下面,我们就来聊聊在使用过程中可能遇到的坑吧......
useState
useState 只在组件首次渲染的时候执行
坑:useState的初始值,只在第一次有效
证据:
当点击按钮修改name的值的时候,我发现在Child组件,是收到了,但是并没有通过useState
赋值给name!
const Child = ({data}) =>{
console.log('child render...', data) // 每次更新都会执行
const [name, setName] = useState(data) // 只会在首次渲染组件时执行
return (
<div>
<div>child</div>
<div>{name} --- {data}</div>
</div>
);
}
const Hook =()=>{
console.log('Hook render...')
const [name, setName] = useState('rose')
return(
<div>
<div>
{count}
</div>
<button onClick={()=>setName('jack')}>update name </button>
<Child data={name}/>
</div>
)
}
想在第一次 render 前执行的代码放 useState() 里面
上面我们已经知道了useState()
只会在第一次渲染的时候才执行,那么这有什么实用价值吗?答案:可以把第一次 render 前执行的代码放入其中。
例如:
const instance = useRef(null);
useState(() => {
instance.current = 'initial value';
});
类似 class component 里的constructor
和componentWillMount
。
useState 里数据必须为 immutable
啥?你还不知道 immutable 是个啥?甩手就是两个链接:Immutable.js 了解一下、Immutable 详解及在 React 实践。
什么是 Immutable Data?
首先,你要知道 JavaScript 中的对象一般是可变的(Mutable Data),因为使用了引用赋值。
Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。
虽然 class component 的 state 也提倡使用 immutable data,但不是强制的,因为只要调用了setState
就会触发更新。
但是使用useState
时,如果在更新函数里传入同一个对象将无法触发更新。
证据:
const [list, setList] = useState([2,32,1,534,44]);
return (
<>
<ol>
{list.map(v => <li key={v}>{v}</li>)}
</ol>
<button
onClick={() => {
// bad:这样无法触发更新!
setList(list.sort((a, b) => a - b));
// good:必须传入一个新的对象!
setList(list.slice().sort((a, b) => a - b));
}}
>sort</button>
</>
)
useState 过时的闭包
之前就说过,Hook 产生问题时,90%都是闭包引起的。下面就来看一下这个诡异的bug:
function DelayCount() {
const [count, setCount] = useState(0);
function handleClickAsync() {
setTimeout(function delay() {
setCount(count + 1); // 问题所在:此时的 count 为5s前的count!!!
}, 5000);
}
function handleClickSync() {
setCount(count + 1);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>异步加1</button>
<button onClick={handleClickSync}>同步加1</button>
</div>
);
}
点击“异步加1”按键,然后立即点击“同步加1”按钮。你会惊奇的发现,count 只更新到 1。
这是因为 delay() 是一个过时的闭包。
来看看这个过程发生了什么:
- 初始渲染:count 值为 0。
- 点击“异步加1”按钮,delay() 闭包捕获 count 的值 0,setTimeout() 5秒后调用 delay()。
- 点击“同步加1”按钮,handleClickSync() 调用 setCount(0 + 1) 将 count 的值设置为 1,组件重新渲染。
- 5秒之后,setTimeout() 执行 delay() 。但是 delay() 中闭包保存 count 的值是初始渲染的值 0,所以调用 setState(0 + 1),结果count保持为 1。
每次 render 都会产生新的闭包。delay() 是一个过时的闭包,它使用在5s前捕获的过时的 count 变量。
为了解决这个问题,可以使用函数方法来更新 count
状态:
function DelayCount() {
const [count, setCount] = useState(0);
function handleClickAsync() {
setTimeout(function delay() {
setCount(count => count + 1); // 重点:setCount传入的回调函数用的是最新的 state!!!
}, 5000);
}
function handleClickSync() {
setCount(count + 1);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>异步加1</button>
<button onClick={handleClickSync}>同步加1</button>
</div>
);
}
关于 Hook 中的闭包:
useEffect
、useMemo
、useCallback
都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state、props)。所以每一次这三种 Hook 的执行,反映的也都是当时的状态,无法获取最新的状态。对于这种情况,应该使用 ref 来访问。
useEffect
如何在 useEffect 中使用 async
上一篇文章中我们提到过:useEffect的 callback 函数要么返回一个能清除副作用的函数,要么就不返回任何内容。
而 async 函数返回的是 Promise 对象,那我们要怎么在 useEffect 的callback 中使用 async 呢?
最简单的方法是IIFE
(自执行函数):
useEffect(() => {
(async () => {
await fetchSomething();
})();
}, []);
useEffect 死循环
- useEffect 在传入第二个参数时一定注意:第二个参数不能为引用类型,会造成死循环。
比如:[]===[] 为false,所以会造成 useEffect 会一直不停的渲染。 - useEffect 的 callback 函数中改变的 state 一定不能在该 useEffect 的依赖数组中。比如:
useEffect(()=>{ setCount(count); }, [count]);
依赖 count,callback 中又 setCount(count)。
推荐启用 eslint-plugin-react-hooks
中的 exhaustive-deps
规则。此规则会在添加错误依赖时发出警告并给出修复建议。
函数作为依赖的时候死循环
有时候,我们需要将函数作为依赖项传入依赖数组中,例如:
// 子组件
let Child = React.memo((props) => {
useEffect(() => {
props.onChange(props.id)
}, [props.onChange, props.id]);
return (
<div>{props.id}</div>
);
});
// 父组件
let Parent = () => {
let [id, setId] = useState(0);
let [count, setCount] = useState(0);
const onChange = (id) => {
// coding
setCount(id);
}
return (
<div>
{count}
<Child onChange={onChange} id={id} /> // 重点:这里有性能问题!!!
</div>
);
};
代码中重点位置,每次父组件render,onChange引用值肯定会变。因此,子组件Child必定会render,子组件触发useEffect,从而再次触发父组件render....循环往复,这就会造成死循环。下面我们来优化一下:
// 子组件
let Child = React.memo((props) => {
useEffect(() => {
props.onChange(props.id)
}, [props.onChange, props.id]);
return (
<div>{props.id}</div>
);
});
// 父组件
let Parent = () => {
let [id, setId] = useState(0);
let [count, setCount] = useState(0);
const onChange = useCallback(() => { // 重点:通过useCallback包裹一层即可达到缓存函数的目的
// coding
}, [id]); // id 为依赖值
return (
<div>
{count}
<Child onChange={onChange} id={id} /> // 重点:这个onChange在每次父组件render都会改变!
</div>
);
};
useCallback
将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将非常有用。
useCallback(fn, deps)
相当于useMemo(() => fn, deps)
useEffect 里面拿不到最新的props和state
useEffect
里面使用到的 state 的值, 固定在了useEffect
内部,不会被改变,除非useEffect
刷新,重新固定 state 的值。
useRef
保存任何可变化的值,.current
属性总是取最新的值。就是相当于全局作用域,一处被修改,其他地方全更新...
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);
});
总结
以上只是收集了一部分工作中可能会遇到的坑,大致分为2种:
- 闭包引起的 state 值过期
- 依赖值监听问题导致死循环
以后遇到其他的问题会继续补充...
参考:
react hooks踩坑记录