Search code examples
javascriptreactjsgraphqlapollo

Calling a Hook only once before the component is rendered (Meteor/React/Apollo/Graphql)


I'm trying to create a feed of things that updates as you swipe down. My logic is that I need to fetch the required data before rendering the feed, and I will fetch new data every time I hit the nth element as I swipe down the page. So I thought of having a boolean state variable to detect the first render, and then I set that to false to not run the query again.

const Feed = (props) => {
    const [feedArray, setFeedArray] = useState([]);
    const [firstRender, setFirstRender] = useState(true);
    const FEED_QUERY = gql`
      query getFeedArray {
        getFeedArray
      }
    `;

    if (firstRender) {
      const { loading, error, data } = useQuery(FEED_QUERY);
      if (loading) return <Loading/>;
      if (error) return `Error! ${error}`;
      feedArray.push(...data.getFeedArray);
      setFirstRender(false);
    }

    return (
      <RenderredComponent />
    );
}
export default withApollo(withRouter(Feed));

Clearly, this results in an error (Unexpected Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.) as the useQuery hook is only called once in the if statement.

I experimented with different ways of doing this, as I require this data before my component is rendered. I tried useEffect() with props.client.query() but that wouldn't work since it allows the gql query to be run synchronously and hence only updates the DOM with the data on the second render, and I can't block the render based on the loading state. I can't place useQuery() in useEffect() as it throws an error as well (hook within a hook).

If I place the useQuery hook outside the if statement, it works but is run every render, which is unnecessary extra calls. For context - renders happen often as I scroll down due to how React Swipeable Views works.

So to put it simply - how can I run the useQuery hook only once, before the component is rendered?

Any help is appreciated, thank you!

EDIT: Screenshots below to illustrate what's happening,

Pic 1 Pic 2 Pic 3

The first screenshot shows when it renders, and I print the data received to the console. I hardcoded a 5-element array to be returned on repeat for this example. Then as I swipe up, the component is being re-rendered although I do NOT need to fetch more data, but it is because the useQuery hook is being triggered. This is just going to keep fetching more data because the component will continually be re-rendered.

Also - I need to store my data in state because I will be adding to it, and there is no other way for my component to remember what was fetched the previous time.

EDIT2: The accepted answer works. But I had a misconception about how useQuery works, I should have done some logging on the server whenever the query was called. This calls the query once and only once, despite all the DOM updates. Furthermore, it adds it to my state only once, because after the first successful render the firstRender state is set to false. Not sure if this is ideal but this is what I would've done if I thought of logging the calls on my server.

const Feed = (props) => {
    const [feedArray, setFeedArray] = useState([]);
    const [firstRender, setFirstRender] = useState(true);
    const FEED_QUERY = gql`
      query getFeedArray {
        getFeedArray
      }
    `;

    const { loading, error, data } = useQuery(FEED_QUERY);
    if (loading) return <Loading/>;
    if (error) return `Error! ${error}`;
    
    if (firstRender) {
      feedArray.push(...data.getFeedArray);
      setFirstRender(false);
    }

    return (
      <RenderredComponent />
    );
}
export default withApollo(withRouter(Feed));

Solution

  • You don't need to use state. Just use query data

    const Feed = props => {
      const FEED_QUERY = gql`
        query getFeedArray {
          getFeedArray
        }
      `;
    
      const { loading, error, data } = useQuery(FEED_QUERY);
    
      if (loading) return <Loading />;
      if (error) return `Error! ${error}`;
    
      return <RenderredComponent data={data.getFeedArray} />;
    };
    export default withApollo(withRouter(Feed));
    

    You can use data wherever you want.

    UPDATE

    If you want to use state for some reason, you can use it with useEffect. Add data.getFeedArray to the useEffect dependencies, so that when it is changed you can set the feedArray state.

    const Feed = props => {
      const [feedArray, setFeedArray] = useState([]);
      const FEED_QUERY = gql`
        query getFeedArray {
          getFeedArray
        }
      `;
    
      const { loading, error, data } = useQuery(FEED_QUERY);
      useEffect(() => {
        setFeedArray([...data.getFeedArray])
      }, [data.getFeedArray])
    
      if (loading) return <Loading />;
      if (error) return `Error! ${error}`;
    
      return <RenderredComponent data={feedArray} />;
    };
    export default withApollo(withRouter(Feed));