#脱围机制

#步骤

  • 使用 ref 引用值
  • 使用 ref 操作 DOM
  • 使用 Effect 同步
  • 你可能不需要 Effect

#使用 ref 操作 DOM

#如何使用 ref 回调管理 ref 列表

ref 回调:

当需要设置 ref 时,React 将传入 DOM 节点来调用你的 ref 回调,并在需要清除它时传入 null

这样就可以维护自己的数组或者 Map

如:

import { useRef } from 'react'

export default function CatFriends() {
  const itemsRef = useRef(null)

  function scrollToId(itemId) {
    const map = getMap()
    const node = map.get(itemId)
    node.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center',
    })
  }

  function getMap() {
    if (!itemsRef.current) {
      // 首次运行时初始化 Map。
      itemsRef.current = new Map()
    }
    return itemsRef.current
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToId(0)}>Tom</button>
        <button onClick={() => scrollToId(5)}>Maru</button>
        <button onClick={() => scrollToId(9)}>Jellylorum</button>
      </nav>
      <div>
        <ul>
          {catList.map((cat) => (
            <li
              key={cat.id}
              ref={(node) => {
                const map = getMap()
                if (node) {
                  map.set(cat.id, node)
                } else {
                  map.delete(cat.id)
                }
              }}
            >
              <img src={cat.imageUrl} alt={'Cat #' + cat.id} />
            </li>
          ))}
        </ul>
      </div>
    </>
  )
}

#访问子组件的 ref

一个组件可以指定将它的 ref “转发”给一个子组件。下面是 MyInput 如何使用 forwardRef API

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />
})

当然,子组件必须是 DOM,函数组件使用 ref 会获取 null

#使用 flushSync 同步更新 state

import { useState, useRef } from 'react'

export default function TodoList() {
  const listRef = useRef(null)
  const [text, setText] = useState('')
  const [todos, setTodos] = useState(initialTodos)

  function handleAdd() {
    const newTodo = { id: nextId++, text: text }
    setText('')
    setTodos([...todos, newTodo])
    listRef.current.lastChild.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
    })
  }

  return (
    <>
      <button onClick={handleAdd}>添加</button>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <ul ref={listRef}>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  )
}

添加一个新的待办事项后,总是滚动到最后一个添加之前的待办事项

因为 setState 调用后立刻调用了 DOM 滚动,此时 DOM 还是旧 DOM,新 state 尚未来得及渲染并绘制新 DOM

所以需要强制 React 同步更新(“刷新”)DOM

从 react-dom 导入 flushSync 并将 state 更新包裹 到 flushSync 调用中:

flushSync(() => {
  setTodos([...todos, newTodo])
})
listRef.current.lastChild.scrollIntoView()

#使用 Effect 同步

Effect 允许你指定由渲染本身,而不是特定事件引起的副作用

Effect 是 react 绘制 dom 后执行

#使用 Effect 异步时需要返回取消函数

如 setTimeout 需要返回 clearTimeout

如 fetch 可以在 then 中处理

import { useState, useEffect } from 'react'
import { fetchBio } from './api.js'

export default function Page() {
  const [person, setPerson] = useState('Alice')
  const [bio, setBio] = useState(null)
  useEffect(() => {
    let ignore = false

    setBio(null)

    fetchBio(person).then((result) => {
      if (!ignore) {
        setBio(result)
      }
    })

    return () => {
      ignore = true
    }
  }, [person])

  return (
    <>
      <select
        value={person}
        onChange={(e) => {
          setPerson(e.target.value)
        }}
      >
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p>
        <i>{bio ?? '加载中……'}</i>
      </p>
    </>
  )
}