[react] useRef forwardRef useImperativeHandle

42 阅读2分钟

useRef

useRef 可以绑定dom

export const App = () => {
  const divRef = useRef(null)
  const handleClick = () => {
    console.dir(divRef.current.style.backgroundColor)
    divRef.current.style.backgroundColor = 'red'
  }
  return (
    <div ref={divRef} onClick={handleClick}>App</div>
  )
}

useRef 可以用来存储值,useRef.current的改变,不会导致组件渲染

export const App = () => {
  console.log('App')
  const divRef = useRef(1)
  const handleClick = () => {
    divRef.current++
    console.log(divRef.current)
  }
  return (
    <div  onClick={handleClick}>App{divRef.current}
    </div>

  )
}

注意事项

  1. 组件在重新渲染的时候,useRef的值不会被重新初始化。
  2. 改变 ref.current 属性时,React 不会重新渲染组件。React 不知道它何时会发生改变,因为 ref 是一个普通的 JavaScript 对象。
  3. useRef的值不能作为useEffect等其他hooks的依赖项,因为它并不是一个响应式状态。
  4. useRef不能直接获取子组件的实例,需要使用forwardRef

forwardRef 父组件传递,子组件接收

const Child = forwardRef((props, ref) => {
  return (
    <div ref={ref}>Child</div>
  )
})

export const App = () => {
  const childRef = useRef(null)
  const handleClick = () => {
    console.log(childRef.current)
  }
  return (
    <div>
      <Child ref={childRef} />
      <button onClick={handleClick}>+1</button>
    </div>
  )
}

useImperativeHandle 自定义子组件暴露给父组件属性 方法,dom

useImperativeHandle第二个参数,可以是 空, 空数组, 依赖项 用法和useEffect一样

类似vue3中的definexpose

1️⃣ forwardRef 的本质

forwardRef 是为了让父组件可以通过 ref 访问子组件内部的某个对象。

默认情况下,如果你直接写:

<div ref={ref}>Child</div>

那么父组件 childRef.current 就会拿到 这个 div 的 DOM 元素


2️⃣ useImperativeHandle 的作用

useImperativeHandle 可以让你自定义暴露给父组件的对象:

useImperativeHandle(ref, () => ({
  name: 'Child',
  handleClick,
  count,
}))

这时候,childRef.current 不再是原来的 DOM 元素,而是你返回的这个对象:

{
  name: 'Child',
  handleClick: function,
  count: 0
}

3️⃣ 为什么“拿不到 DOM”

你的原代码里:

<div ref={ref} onClick={handleClick}>Child{count}</div>

这里 refuseImperativeHandle 覆盖了:

  • useImperativeHandle 返回的对象会覆盖 ref 的默认值。
  • 所以 childRef.current 不再是 div,而是你自定义的对象。
  • 如果你想在 useImperativeHandle 里同时保留 DOM,需要手动加上:
const divRef = useRef(null)

useImperativeHandle(ref, () => ({
  name: 'Child',
  handleClick,
  count,
  div: divRef.current // 手动暴露 DOM
}))

return (
  <div ref={divRef} onClick={handleClick}>
    Child{count}
  </div>
)

然后父组件访问 DOM 就是:

childRef.current.div
const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(count + 1)
  }
  const divRef = useRef(null)
  useImperativeHandle(ref, () => ({
    name: 'Child',
    handleClick,
    count,
    dom:divRef.current
  }))
  return (
    <div ref={divRef} onClick={handleClick}>
      Child{count}
    </div>
  )
})

export const App = () => {
  const childRef = useRef(null)
  const handleClick = () => {
    console.dir(childRef.current.dom)
  }
  return (
    <div>
      <Child ref={childRef} />
      <button onClick={handleClick}>获取子组件</button>
    </div>
  )
}

✅ 总结

  • forwardRef + 直接 ref → 拿 DOM
  • useImperativeHandle → ref 被覆盖,拿到的是你返回的对象
  • 想要同时拿对象和 DOM → 在 useImperativeHandle 返回对象里手动加一个 DOM 引用

useRef实际例子

export const App = () => {
  let time = useRef(null)
  const [count, setCount] = useState(0)
  function handleClickstart(){
    time.current = setInterval(() => {
      setCount(prev => prev + 1)
    }, 1000)
  }
  function handleClickstop(){
    clearInterval(time.current)
  }
  return (
    <div >
      <div onClick={handleClickstart}>App{count}</div>
      <button onClick={handleClickstop}>停止</button> 
    </div>

  )
}