背景
最近在看 react 新的官方文档
的时候,看到这么一个标题,How to manage a list of refs using a ref callback,就是一个图片的列表,类似这样
然后点击按钮的时候,通过 scrollIntoView
这个 api
来让他滚动,要使用这个 api
我们就要通过 ref
拿到这个 dom
元素,但是如果是一个列表的话,我们要怎么去做这个事情呢?难道循环用 useRef
申明一堆 ref
引用吗?显然不合理。
问题
文档里头还给了一段代码
<ul>
{items.map((item) => {
// Doesn't work!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
显然这段代码肯定是不可行的,都跑不起来,因为 hooks
他只能在组件的最顶层,是不可以放在判断语句或者循环语句里头的
解决方法 ref callback
ref callback
就是给 ref
传一个函数,react
在设置 ref
的值的时候,会自动去调用这个函数,然后我们就可以通过这个函数去维护一个数组或者一个 map
,从而实现我们需要多个 ref
的需求。听着好像挺复杂的,上代码吧
const RefListDemo = () => {
const itemsRef = useRef(null)
function getMap() {
if (!itemsRef.current) {
itemsRef.current = new Map()
}
return itemsRef.current
}
function scrollToId(itemId) {
const map = getMap()
const node = map.get(itemId)
node.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center',
})
}
return (
<>
<nav>
<button onClick={() => scrollToId(0)}>Tom</button>
<button onClick={() => scrollToId(5)}>Maru</button>
<button onClick={() => scrollToId(9)}>Jellylorum</button>
</nav>
<div>
<ul
style={{
listStyle: 'none',
display: 'flex',
width: 300,
overflow: 'scroll',
flexWrap: 'nowrap',
}}
>
{catList?.map((cat) => (
<li
key={cat.id}
style={{ marginLeft: 12 }}
// 用这样的方式,就可以拿到每个节点,并进行维护了
ref={(node) => {
const map = getMap()
node ? map.set(cat.id, node) : map.delete(cat.id)
}}
>
<img src={cat.imageUrl} alt={'Cat #' + cat.id} />
</li>
))}
</ul>
</div>
</>
)
}
上面代码的关键就在这里,我们可能平时传递 ref
的时候,都是传递一个具体的引用,好像都比较少用到这种传递函数的方式,官方是使用 map
维护,用列表也是一样的逻辑。
ref={(node) => {
const map = getMap()
node ? map.set(cat.id, node) : map.delete(cat.id)
}}
代码示例我放到这里啦。如果有兴趣的话,可以来这里试一下
先有 Dom
再滚动
在文档里头还有说到另一个问题,像这样在输入框输入,然后插入到队列里头,然后还是用 scrollIntoView
这个 api
滚动到新插入的结点的位置,但是这就可能会有一个问题,我们 setTodos
之后就执行了 scrollIntoView
但是我们都知道在
react
里头,更新state
都是在队列里头更新的,并不是每次setState
的时候就会立即更新, 所以更新上去的dom
可能还没插入进去,我们就执行了scrollIntoView
,他不是一个一定会出现的问题,但是如果在实现类似的需求的时候,我们就得意识到这是个潜在的问题,
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();
flushSync
在 react-dom
里头提供了一个 api
,可以让我们实现同步更新 dom
, 就是 flushSync
,在 setTodos
的时候,用 flushSync
就能避免这样的问题发生
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();