12. useRef 除了获取 DOM 还能做什么?(存储可变值、避免重渲染)

10 阅读3分钟

12. useRef 除了获取 DOM 还能做什么?(存储可变值、避免重渲染)

答案:

useRef 的基本用法:

1. 获取 DOM 元素

function MyComponent() {
  const inputRef = useRef(null)

  const focusInput = () => {
    inputRef.current.focus()
  }

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  )
}

2. 存储可变值(不触发重渲染)

function MyComponent() {
  const [count, setCount] = useState(0)
  const renderCount = useRef(0)
  const previousCount = useRef(0)

  // 每次渲染时更新渲染次数
  renderCount.current += 1

  // 保存上一次的 count 值
  useEffect(() => {
    previousCount.current = count
  })

  return (
    <div>
      <p>Current count: {count}</p>
      <p>Previous count: {previousCount.current}</p>
      <p>Render count: {renderCount.current}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

3. 存储定时器 ID

function TimerComponent() {
  const [seconds, setSeconds] = useState(0)
  const intervalRef = useRef(null)

  const startTimer = () => {
    if (intervalRef.current) return // 防止重复启动

    intervalRef.current = setInterval(() => {
      setSeconds((prev) => prev + 1)
    }, 1000)
  }

  const stopTimer = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current)
      intervalRef.current = null
    }
  }

  useEffect(() => {
    return () => {
      // 组件卸载时清理定时器
      if (intervalRef.current) {
        clearInterval(intervalRef.current)
      }
    }
  }, [])

  return (
    <div>
      <p>Seconds: {seconds}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  )
}

4. 存储前一次的值

function usePrevious(value) {
  const ref = useRef()

  useEffect(() => {
    ref.current = value
  })

  return ref.current
}

function MyComponent() {
  const [count, setCount] = useState(0)
  const prevCount = usePrevious(count)

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

5. 存储组件实例

function ParentComponent() {
  const childRef = useRef(null)

  const handleChildAction = () => {
    // 调用子组件的方法
    childRef.current?.doSomething()
  }

  return (
    <div>
      <button onClick={handleChildAction}>Trigger Child Action</button>
      <ChildComponent ref={childRef} />
    </div>
  )
}

const ChildComponent = forwardRef((props, ref) => {
  const [data, setData] = useState(null)

  useImperativeHandle(ref, () => ({
    doSomething: () => {
      console.log('Child component action triggered')
      setData('Action performed')
    },
  }))

  return <div>{data}</div>
})

6. 避免闭包陷阱

function MyComponent() {
  const [count, setCount] = useState(0)
  const countRef = useRef(count)

  // 更新 ref 的值
  countRef.current = count

  useEffect(() => {
    const timer = setInterval(() => {
      // 使用 ref 获取最新值,避免闭包陷阱
      console.log('Current count:', countRef.current)
    }, 1000)

    return () => clearInterval(timer)
  }, []) // 空依赖数组,只在挂载时执行

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

7. 存储表单数据

function FormComponent() {
  const formDataRef = useRef({
    name: '',
    email: '',
    message: '',
  })

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('Form data:', formDataRef.current)
    // 提交表单数据
  }

  const updateField = (field, value) => {
    formDataRef.current[field] = value
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Name"
        onChange={(e) => updateField('name', e.target.value)}
      />
      <input
        type="email"
        placeholder="Email"
        onChange={(e) => updateField('email', e.target.value)}
      />
      <textarea
        placeholder="Message"
        onChange={(e) => updateField('message', e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  )
}

8. 存储动画状态

function AnimatedComponent() {
  const [isVisible, setIsVisible] = useState(false)
  const animationRef = useRef(null)

  const startAnimation = () => {
    if (animationRef.current) {
      cancelAnimationFrame(animationRef.current)
    }

    let startTime = null
    const duration = 1000 // 1秒

    const animate = (timestamp) => {
      if (!startTime) startTime = timestamp
      const progress = (timestamp - startTime) / duration

      if (progress < 1) {
        // 执行动画
        console.log('Animation progress:', progress)
        animationRef.current = requestAnimationFrame(animate)
      } else {
        console.log('Animation completed')
      }
    }

    animationRef.current = requestAnimationFrame(animate)
  }

  useEffect(() => {
    return () => {
      if (animationRef.current) {
        cancelAnimationFrame(animationRef.current)
      }
    }
  }, [])

  return (
    <div>
      <button onClick={startAnimation}>Start Animation</button>
      <div style={{ opacity: isVisible ? 1 : 0 }}>Animated Content</div>
    </div>
  )
}

9. 存储缓存数据

function useCache() {
  const cacheRef = useRef(new Map())

  const get = (key) => {
    return cacheRef.current.get(key)
  }

  const set = (key, value) => {
    cacheRef.current.set(key, value)
  }

  const clear = () => {
    cacheRef.current.clear()
  }

  return { get, set, clear }
}

function DataComponent({ id }) {
  const [data, setData] = useState(null)
  const { get, set } = useCache()

  useEffect(() => {
    // 先检查缓存
    const cachedData = get(id)
    if (cachedData) {
      setData(cachedData)
      return
    }

    // 缓存中没有,从服务器获取
    fetchData(id).then((result) => {
      setData(result)
      set(id, result) // 存入缓存
    })
  }, [id, get, set])

  return <div>{data ? data.title : 'Loading...'}</div>
}

10. 存储 WebSocket 连接

function ChatComponent() {
  const [messages, setMessages] = useState([])
  const wsRef = useRef(null)

  useEffect(() => {
    // 建立 WebSocket 连接
    wsRef.current = new WebSocket('ws://localhost:8080')

    wsRef.current.onmessage = (event) => {
      const message = JSON.parse(event.data)
      setMessages((prev) => [...prev, message])
    }

    return () => {
      // 组件卸载时关闭连接
      if (wsRef.current) {
        wsRef.current.close()
      }
    }
  }, [])

  const sendMessage = (text) => {
    if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify({ text }))
    }
  }

  return (
    <div>
      {messages.map((msg, index) => (
        <div key={index}>{msg.text}</div>
      ))}
      <button onClick={() => sendMessage('Hello!')}>Send Message</button>
    </div>
  )
}

useRef vs useState 的区别:

特性useRefuseState
触发重渲染
存储类型任何值任何值
更新方式ref.current = valuesetValue(value)
访问方式ref.current直接访问
用途存储可变值、DOM 引用存储状态

最佳实践:

  1. 明确用途:区分 DOM 引用和可变值存储
  2. 及时清理:在 useEffect 清理函数中清理定时器、事件监听器等
  3. 避免滥用:不要用 useRef 替代 useState,除非确实不需要触发重渲染
  4. 类型安全:使用 TypeScript 时,为 ref 指定正确的类型