react-apollo-hooks icon indicating copy to clipboard operation
react-apollo-hooks copied to clipboard

Trying to get my head around fetching data only once, thought useQuery works like useEffect

Open rmoskal opened this issue 6 years ago • 6 comments

When using useQuery I've noticed that if I change some state on my component, the query gets rerun as one would expect.

function Index() {
    ...
    const [dialogOpen, setDialogOpen] = useState({ show: false, id: '0' });
    ...   
    const { data, error } = useQuery(GET_JOBS, { suspend: true });
    if (error) {
        return <div>Error! {error.message}</div>;
    }
    const jobs = data.cxJobs; //This is our data
    ....
    function editCallback(e, more) {
        setDialogOpen({ show: true, id: e });
    }
....
}

Since I only want it to happen the first time it renders, I assumed it worked like useffect, and that by adding an empty array to the end of the call: const { data, error } = useQuery(GET_JOBS, { suspend: true }, []); It turns out that's the not the case. Plus it seems to me there's no easy way to make that happen with useQuery. You can't call a hook conditionally.

Do I need to fallback to using the client directly?

rmoskal avatar May 08 '19 15:05 rmoskal

You could utilise skip option in useQuery, the query should be cached so it shouldn't make an actual api call in the first place, but you could have a state dataExist and once data is returned from useQuery you could set dataExist state to true and skip the query.

jinshin1013 avatar May 09 '19 06:05 jinshin1013

Thanks, I will experiment with this. I knew there had to be a better way than what I was thinking about doing!

rmoskal avatar May 09 '19 14:05 rmoskal

For anyone interested. First I followed @jinshin1013s advice. This is what I wound up doing. It does seem pretty inelegant: super fiddly and imperative:

function Index() {
    ...
    const [jobs, setJobs] = useState([]);
    const [dialogOpen, setDialogOpen] = useState({ show: false, id: '0' });
    ...   
    const queryOptions =jobs.length >0? { suspend: true, skip:true}: { suspend: true };
    const { data, error } = useQuery(GET_JOBS, queryOptions);
    if (error) {
        return <div>Error! {error.message}</div>;
    }
    const _jobs = get('cxJobs', data); //This is our data
    if(_jobs)
        setJobs(_jobs); //Flip the state so we only do it once
    ....
    function editCallback(e, more) {
        setDialogOpen({ show: true, id: e });
    }
....
}

I rewrote it using useEffect and it's better. I'm just wondering if it be better still.


    const [dialogOpen, setDialogOpen] = useState({ show: false, id: '0' });
    const [jobs, setJobs] = useState([]);

    useEffect(()=> {fetchData()}, []);

    const fetchData = async _ => {
        const result = await client.query({query:GET_JOBS});
        setJobs(get(['data','cxJobs'], result));
    }

    async function onEventChanged(id, event) {
        await mutateOne(client, jobGQL, eventToJob(event));
    }

rmoskal avatar May 09 '19 16:05 rmoskal

You could do something like this. Also, you might also want to avoid setting the result of the query to useState and just use data directly.

const [skip, setSkip] = React.useState(false)
const { loading, data } = useQuery(QUERY, { skip })

React.useEffect(() => {
  // check whether data exists
  if (!loading && !!data) {
    setSkip(true)
  }
}, [data, loading])

if (loading) return <Loading />

// safe to assume data exist
return <Comp jobs={data.cxJobs} />

jinshin1013 avatar May 10 '19 04:05 jinshin1013

What if the same query is being used at two different components and I want it to fetch only once and serve it from cache in the other component?

vish30 avatar Apr 06 '21 09:04 vish30

@vish30 — at this point you should be using the useQuery hook from ApolloClient! What version of ApolloClient are you on?

fbartho avatar Apr 06 '21 16:04 fbartho