Latest Ref
Loading "Intro to Latest Ref"
Run locally for transcripts
One liner: The Latest Ref Pattern allows you to have a reference to the
latest value of a prop, state, or callback without needing to list it in a
dependency array when accessing it in a
useEffect
.When React introduced hooks it did more than give us an excellent primitive with
super abstract-ability powers. It also changed an important default that results
in fewer bugs deployed to production. They changed how you access the latest
value of state and props.
Before, you would access these values via
this.state
and this.props
meaning
that you'd always get the latest value of state or props in your functions.
Let's explore an example: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>
)
}
}
Think about that
feedPet
function for a moment... What kinds of bugs can you
spot with this implementation? Let me ask you a question. What would happen if
the pet.canEat
function took a couple seconds to resolve? What could the user
do to cause a problem? If the user changed the selectedPetFood
what could
happen? Yeah! You could check whether your bird can eat worms but then actually
feed it grass! Or what if while we're checking whether your dog can eat some
candy the props changed now we're withholding the candy from a hungry crab! ๐ข
(I mean, like crab-candy... I don't know if that's a thing... ๐
)Can you imagine how we could side-step these issues? It's simple actually:
class PetFeeder extends React.Component {
// ...
feedPet = async () => {
const { pet } = this.props
const { selectedPetFood } = this.state
const canEat = await pet.canEat(selectedPetFood)
if (canEat) {
pet.eat(selectedPetFood)
}
}
// ...
}
The default led to bugs that were hard to catch because they often didn't happen
locally, but if there's one thing I know it's that everything that can happen,
users will make happen eventually ๐
And unfortunately these kinds of bugs are
also difficult to reproduce. So I made it a habit of doing this when I was
writing React class components back in the day and I was able to avoid this
problem altogether most of the time.
As mentioned earlier, React hooks flipped this default on its head and now every
function references the closure-version of props and state values rather than
accessing the latest value off some component instance. So now the kinds of bugs
you experience will happen during your local development. You won't be able to
avoid them, so you'll ship fewer (at least, that's been my experience).
So let's rewrite the example above with 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>
)
}
Alright, so that's the default (by that I mean it's the more natural and easier
way to write it). It avoids the bugs mentioned. But what if we wanted the
behavior before? Could we make that work with hooks? Sure! We just need some way
to reference the latest version of a value.
useRef
to the rescue!function PetFeeder({ pet }) {
const [selectedPetFood, setSelectedPetFood] = useState(null)
const latestPetRef = useRef(pet)
const latestSelectedPetFoodRef = useRef(selectedPetFood)
// why is the useEffect necessary? Because side-effects run in the function
// body of your component can lead to some pretty confusing bugs. Just keep
// your function body free of side-effects and you'll be better off.
useEffect(() => {
latestPetRef.current = pet
latestSelectedPetFoodRef.current = selectedPetFood
// Wondering why we have no dependency list? Do we really need it?
// Not really... So we don't bother.
})
const feedPet = async () => {
const canEat = await latestPetRef.current.canEat(
latestSelectedPetFoodRef.current,
)
if (canEat) {
latestPetRef.current.eat(latestSelectedPetFoodRef.current)
}
}
return (
<div>
<PetFoodChooser onSelection={food => setSelectedPetFood(food)} />
<button onClick={feedPet}>Feed {pet.name}</button>
</div>
)
}
We've successfully simulated the class version of our original component. The
ref
+ useEffect
there is what makes up the latest ref pattern.Now why is this a desirable pattern you might ask? In the example above it looks
like you'd never want to deal with those bugs we talked about right. Well it
turns out there are some situations where you really do want the latest
version of the callback. Use cases vary, but one popular library that uses this
pattern heavily is
react-query
. They use
this for query and mutation functions/configuration. One reason this is so
useful is because it means they can call your callback in a useEffect
without
referencing it in the dependency list. For example:function useExampleOne(callback) {
useEffect(() => {
callback()
}, [callback]) // <-- have to include the callback in the dep array
}
function useExampleTwo(callback) {
const latestCallbackRef = useRef(callback)
useEffect(() => {
latestCallbackRef.current = callback
})
useEffect(() => {
latestCallbackRef.current()
}, []) // <-- don't have to include the callback in the dep array
}
It's important that you understand the trade-offs here! Remember, when we do
this we're going back to the class component default. So just think about the
unexpected behavior's you'll get when you switch the default like this.
๐ For more on hooks and closures, check
Getting Closure on React Hooks
๐ For more on this subject, read
How React Uses Closures to Avoid Bugs.
๐ For more on the latest ref pattern, read
The Latest Ref Pattern in React.
Real World Projects that use this pattern: