Search code examples
reactjsreact-hooksapolloapollo-client

Which is the better way to read Apollo reactive variables


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:

  • either by using the hook useReactiveVar - const myVar = useReactiveVar(myReactiveVar);
  • or simply calling the reactive var without arguments 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:

  • We can either reference the state directly - setState(count + 1);.
  • Or we pass a function - 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! :)


Solution

  • 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.

    Screenshot

    To solve this, we have 2 options:

    1. Update the context of useEffect by adding count as a dependency
      React.useEffect(() => {
        const id = setInterval(() => setCount(count + 1), 1000)
        
    
        return () => clearInterval(id)
      }, [count]) <--------- CHANGE 
    
    
    1. Pass a function to setCount - which gives us access to the previous state parameter
    React.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>
      </>)
    }
    

    Here's what we get: Screenshot

    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>
      </>)
    }
    

    Screenshot

    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!