I was following a couple of tutorials about the usage of reactive variables as a state management solution in a react/apollo client app, and I noticed there 2 ways to reference the current value of a reactive variable:
useReactiveVar
- const myVar = useReactiveVar(myReactiveVar);
const myVar = myReactiveVar();
So my question is:
is there a benefit for using one way of referencing the reactive variable over the other and if so, then why?
I have a theory that the ways of referencing the current value of the reactive variable are similar to how setting state based on current state is used:
setState(count + 1);
.setState((prev) => prev + 1)
.
The second way is considered "safer" as it guarantees an accurate read of the current state during asynchronous code. I couldn't find out whether my theory is correct though!This is a simple component where I use both ways and both are working in both instances where reading the current value of the reactive variable is used:
import React from 'react'
import { useQuery, useReactiveVar } from '@apollo/client';
import { missionsLimitRV } from '../../apollo/client';
import { GET_MISSIONS } from '../../data/queries';
export const Missions = () => {
const limit = useReactiveVar(missionsLimitRV); <---here--<<
const { data, loading } = useQuery(GET_MISSIONS, {
variables: {
limit: limit
}
});
const addMission = () => {
missionsLimitRV(missionsLimitRV() + 1) <---here-<<
}
if (loading) {
return <h2>Loading...</h2>
}
if (!data.missions.length) {
return <h2>No Missions Available</h2>
}
const missions = data.missions;
console.log(missions);
return (
<div>
<button onClick={addMission}>add mission</button>
{ missions.map((mission) => (
<div key={mission.id}>
<h2>{mission.name}</h2>
<ul>
{mission?.links?.map((link) => (
<li key={link}><a href={link}>{link}</a></li>
))}
</ul>
</div>
)) }
</div>
);
};
Thanks for reading! :)
Just came across this question - I know it's been some time since you asked but this concept has caused me a lot of confusion as well so hopefully this helps someone. Here's my take:
I think the reason is a bit different than your theory -- the local state issue is due to stale closure. The reactive variable reason has to do with how rendering is triggered in React.
The stale closure problem
Here's a simple illustration of the counter example you mentioned.
const TimerLocalState = () => {
const [count, setCount] = React.useState(0)
const logCount = () => {
console.info(count)
}
React.useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000)
return () => clearInterval(id)
}, [])
return (
<>
<h1>{count}</h1>
<button onClick={logCount}/>
</>)
}
}
In this component, the counter value displayed will go to 1 and then stay there. The reason for that is because the value of count
is not being updated in the context in which setCount
is executed. Similarly, if you press Log Count
, the value logged will always be 1.
To solve this, we have 2 options:
useEffect
by adding count
as a dependency React.useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000)
return () => clearInterval(id)
}, [count]) <--------- CHANGE
setCount
- which gives us access to the previous state parameterReact.useEffect(() => {
const id = setInterval(() => setCount((prevCount) => prevCount + 1), 1000) <--------- CHANGE
return () => clearInterval(id)
}, [])
With Apollo, we don't have the same problem, which is why it works both ways in your code, as long as the component references the value programmatically.
const countVar = makeVar(0)
const TimerApolloState = () => {
const logCountVar = () => {
console.info(countVar())
}
React.useEffect(() => {
const id = setInterval(() => {
const newVal = countVar() + 1;
countVar(newVal)
}, 1000)
return () => clearInterval(id)
}, [])
console.info("Render")
return (
<>
<h1>{countVar()}</h1>
<button onClick={logCountVar}>Log CountVar</button>
</>)
}
The text doesn't update because the component only renders once, on mount. But if we click the Log CountVar
button, the component still has access to the latest value.
Hence, using countVar()
works in examples like yours where you're programmatically referencing the store variable. It's especially useful if you need to reference the latest value of the store variable but otherwise you don't want each change in its value to trigger a re-render.
But, if you need a change in the value to trigger a re-render in the component, that's where useReactiveVar
comes in handy.
const TimerApolloState = () => {
const count = useReactiveVar(countVar) <--------- declare in component
const logCountVar = () => {
console.info(countVar())
}
React.useEffect(() => {
const id = setInterval(() => {
const newVal = countVar() + 1;
countVar(newVal)
}, 1000)
return () => clearInterval(id)
}, [])
console.info("Render")
return (
<>
<h1>{count}</h1> <--------- this will now be up to date
<button onClick={logCountVar}>Log CountVar</button>
</>)
}
Lastly, if you look at the source code for useReactiveVar
--
export function useReactiveVar(rv) {
var value = rv();
var setValue = useState(value)[1];
useEffect(function () {
var probablySameValue = rv();
if (value !== probablySameValue) {
setValue(probablySameValue);
}
else {
return rv.onNextChange(setValue); <------ event listener updates local state value
}
}, [value]);
return value; <------ return local state value, which will trigger re-render if the value changes
}
It looks like they use an event emitter similar to onchange
whenever the value of the store variable changes. The event listener then invokes setValue
and returns value
, which triggers a re-render.
I hope this helps!