HomeAbout Me

Advanced React Patterns: Latest Ref

By Daniel Nguyen
Published in React JS
July 03, 2025
2 min read
Advanced React Patterns: Latest Ref

🔁 The Latest Ref Pattern in React: Solving Stale Closure Problems Without Breaking Hooks

One-liner: The Latest Ref Pattern helps you access the most recent value of a prop, state, or callback without needing to list it in a `useEffect` dependency array.

💡 The Problem with Closures in React Hooks

React Hooks gave us a powerful and expressive way to write components. But they also introduced a subtle shift in behavior: functions within your component “remember” the values from when they were created. This is due to how JavaScript closures work.

Let’s illustrate this with an example.

React Class Components: Always Get the Latest Props/State

In class components, accessing the latest values was straightforward:

class PetFeeder extends React.Component {
state = { selectedPetFood: null }
feedPet = async () => {
const canEat = await this.props.pet.canEat(this.state.selectedPetFood)
if (canEat) {
this.props.pet.eat(this.state.selectedPetFood)
}
}
render() {
return (
<div>
<PetFoodChooser
onSelection={selectedPetFood => this.setState({ selectedPetFood })}
/>
<button onClick={this.feedPet}>Feed {this.props.pet.name}</button>
</div>
)
}
}

⚠️ The Bug

Now imagine the user selects “worms” and clicks “Feed.” While waiting for canEat(worms) to return, they change their selection to “grass.” When the check completes, we might mistakenly feed the pet grass instead of worms — even though the check was for worms.

This kind of bug is subtle, hard to reproduce, and very real in asynchronous code.

✅ The Fix (In Class Components)

You could solve this by caching the props/state at the time the function runs:

feedPet = async () => {
const { pet } = this.props
const { selectedPetFood } = this.state
const canEat = await pet.canEat(selectedPetFood)
if (canEat) {
pet.eat(selectedPetFood)
}
}

This ensures the values being used are snapshotted at the time of the action — no surprises.


⚙️ React Hooks Flip the Default

Hooks changed the game. Now, closures capture whatever values existed when the function was defined.

Here’s the same PetFeeder component using hooks:

function PetFeeder({ pet }) {
const [selectedPetFood, setSelectedPetFood] = useState(null)
const feedPet = async () => {
const canEat = await pet.canEat(selectedPetFood)
if (canEat) {
pet.eat(selectedPetFood)
}
}
return (
<div>
<PetFoodChooser onSelection={food => setSelectedPetFood(food)} />
<button onClick={feedPet}>Feed {pet.name}</button>
</div>
)
}

At first glance, this seems fine — and it usually is. But if feedPet is used inside an effect or another memoized callback, and selectedPetFood changes, the stale value could be used.


🧰 The Latest Ref Pattern to the Rescue

Sometimes, you want to use the latest value without re-creating your function every time the value changes.

Here’s how:

function PetFeeder({ pet }) {
const [selectedPetFood, setSelectedPetFood] = useState(null)
const latestPetRef = useRef(pet)
const latestSelectedPetFoodRef = useRef(selectedPetFood)
useEffect(() => {
latestPetRef.current = pet
latestSelectedPetFoodRef.current = selectedPetFood
}, [pet, selectedPetFood])
const feedPet = async () => {
const canEat = await latestPetRef.current.canEat(
latestSelectedPetFoodRef.current
)
if (canEat) {
latestPetRef.current.eat(latestSelectedPetFoodRef.current)
}
}
return (
<div>
<PetFoodChooser onSelection={setSelectedPetFood} />
<button onClick={feedPet}>Feed {pet.name}</button>
</div>
)
}

This pattern lets you define feedPet just once, and always get the current values of pet and selectedPetFood.


🤯 Why Is This Useful?

There are real-world use cases where you need to:

  • Avoid re-creating functions every time state or props change.
  • Maintain long-lived timers, debouncers, or event listeners that always act on the latest value.
  • Prevent unnecessary re-renders or avoid dependency churn in useEffect.

One popular example is react-query, where internal logic can call user-defined callbacks from within effects — without needing them in a dependency array.


🧪 Demo: Debounced Increment with Latest Ref

Let’s look at a debounced counter where this pattern becomes essential.

Scenario

You have:

  • A step input (default: 1)
  • A counter button that increments the count by step
  • A debounce delay of 3 seconds

What Should Happen?

  1. The user clicks the button (step = 1)
  2. Before the timer finishes, they change the step to 2
  3. They click again
  4. After the timer finishes, both clicks should increment the count by 2

The Problem

Here’s the naive setup:

const increment = () => setCount(c => c + step)
const debouncedIncrement = useDebounce(increment, 3000)

Every time step changes, a new increment function is created, so the debounce timer gets reset and doesn’t cancel properly.

Attempted Fix with useCallback

const increment = useCallback(() => setCount(c => c + step), [step])
const debouncedIncrement = useDebounce(increment, 3000)

Still no luck — a new increment gets created when step changes.

✅ Real Fix: Use Latest Ref

Inside your useDebounce hook, store the latest version of the callback in a ref:

function useDebounce(callback, delay) {
const latestCallbackRef = useRef(callback)
useEffect(() => {
latestCallbackRef.current = callback
}, [callback])
return useMemo(() => {
return debounce(() => latestCallbackRef.current(), delay)
}, [delay])
}

Now, no matter how often callback changes, the debounced function always has access to the most recent version of it — and the debounce timer stays consistent.


🧠 Understand the Trade-Offs

Yes, this pattern is powerful — but it comes with responsibility:

  • You’re bypassing the default behavior of closures
  • You may miss dependencies in effects, which could lead to bugs if used incorrectly
  • It’s best suited for specific cases like debouncing, memoization, and async callbacks

📚 Learn More


✅ Key Takeaways

  • React hooks close over variables at the time they’re defined — this can lead to stale values.
  • The Latest Ref Pattern gives you access to the current value without recreating your function.
  • Use this pattern when building debounced functions, long-lived event handlers, or libraries that depend on stable references.

Want to experiment with this pattern? Try building a debounced search input or autocomplete box using useDebounce and the Latest Ref Pattern — you’ll see its power firsthand.

Happy coding! 🧪💡



Tags

#ReactPatterns

Share

Previous Article
Advanced React Patterns: Composition

Table Of Contents

1
💡 The Problem with Closures in React Hooks
2
⚙️ React Hooks Flip the Default
3
🧰 The Latest Ref Pattern to the Rescue
4
🤯 Why Is This Useful?
5
🧪 Demo: Debounced Increment with Latest Ref
6
🧠 Understand the Trade-Offs
7
✅ Key Takeaways

Related Posts

Advanced React Patterns: Control Props
July 09, 2025
2 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media