Skip to content

use-synced-ref

A React hook that creates a ref that automatically stays in sync with the provided value, ensuring you always have access to the latest state in asynchronous operations.

Features

  • Always current: Ref automatically updates to reflect the latest value
  • Stale closure prevention: Prevents accessing stale values in async operations
  • Simple API: Minimal interface with automatic synchronization
  • Type safe: Full TypeScript support with generic typing

Problem It Solves

Stale Closure Problem

Problem: In React, when you capture state values in closures (like in setTimeout, event handlers, or async operations), you might get stale values due to how JavaScript closures work.

tsx
// ❌ Problematic approach - stale closure issue
function Component() {
   const [count, setCount] = useState(0)

   const handleAsyncOperation = () => {
      setTimeout(() => {
         // This will always log the value of count when handleAsyncOperation was called
         // Not the current value after 2 seconds
         console.log('Stale count:', count) // Might be outdated!
      }, 2000)
   }

   return (
      <div>
         <p>Count: {count}</p>
         <button onClick={() => setCount((c) => c + 1)}>Increment</button>
         <button onClick={handleAsyncOperation}>Log count after 2s</button>
      </div>
   )
}

Solution:

  • Provides a ref that always contains the current value
  • Eliminates stale closure issues in async operations
  • Ensures you can access the latest state regardless of when the closure was created
tsx
// ✅ Clean approach with useSyncedRef
function Component() {
   const [count, setCount] = useState(0)
   const countRef = useSyncedRef(count)

   const handleAsyncOperation = () => {
      setTimeout(() => {
         // countRef.current always has the latest value
         console.log('Current count:', countRef.current) // Always up-to-date!
      }, 2000)
   }

   return (
      <div>
         <p>Count: {count}</p>
         <button onClick={() => setCount((c) => c + 1)}>Increment</button>
         <button onClick={handleAsyncOperation}>Log count after 2s</button>
      </div>
   )
}
Performance Benefits
  • Lightweight solution with minimal overhead
  • No unnecessary re-renders or effect dependencies
  • Simple ref assignment on every render ensures synchronization

Parameters

ParameterTypeRequiredDefault ValueDescription
stateT-The value to keep synchronized with the ref

Return Value(s)

This hook returns the ref version of the provided state, which auto syncs up

Return ValueTypeDescription
syncedRefRefObject<T>A mutable ref object whose .current property always contains the latest state value

Common Use Cases

  • Async operations: Accessing latest state in setTimeout, setInterval, or API calls
  • Event handlers: Ensuring event callbacks have access to current state without stale closures
  • WebSocket handlers: Accessing current state in WebSocket message handlers

Usage Examples

Basic State Synchronization

ts
import { useState } from 'react'
import { useSyncedRef } from 'classic-react-hooks'

export default function Counter() {
   const [count, setCount] = useState(0)
   const countRef = useSyncedRef(count)

   const handleAsyncOperation = () => {
      setTimeout(() => {
         // countRef.current always has the latest value
         console.log('Current count:', countRef.current)
         alert(`Count is now: ${countRef.current}`)
      }, 2000)
   }

   return (
      <div>
         <p>Count: {count}</p>
         <button onClick={() => setCount((c) => c + 1)}>Increment</button>
         <button onClick={handleAsyncOperation}>Show count after 2 seconds</button>
      </div>
   )
}

Event Handlers with Latest State

Example
ts
import { useState, useCallback } from 'react'
import { useSyncedRef } from 'classic-react-hooks'

export default function EventExample() {
   const [messages, setMessages] = useState<string[]>([])
   const messagesRef = useSyncedRef(messages)

   const handleKeyPress = useCallback((event: KeyboardEvent) => {
      if (event.key === 'Enter') {
         // Always access the latest messages array
         const currentMessages = messagesRef.current
         console.log('Current messages count:', currentMessages.length)

         setMessages((prev) => [...prev, `Message ${prev.length + 1}`])
      }
   }, []) // No need to include messages in dependencies

   useEffect(() => {
      document.addEventListener('keydown', handleKeyPress)
      return () => document.removeEventListener('keydown', handleKeyPress)
   }, [handleKeyPress])

   return (
      <div>
         <p>Press Enter to add messages</p>
         <ul>
            {messages.map((msg, index) => (
               <li key={index}>{msg}</li>
            ))}
         </ul>
      </div>
   )
}