Search code examples
javascriptreactjsreact-hooksapolloreact-apollo

Why does useQuery call cause a re-render in my component?


As we know a react component is re-rendered when it's props or state changes.

Now i'm using useQuery from react-apollo package like below:

import { gql, useQuery } from '@apollo/client';

const getBookQuery = gql`
  {
    books {
      name
    }
  }
`;

function BookList() {
    const { loading, error, data} = useQuery(getBookQuery);

    if(loading) return <p>Loading....</p>
    if(error) return <p>Ops! Something went wrong</p>

    return (
      <>
        <ul>
          {data.books.map(book => (
            <li key={book.name}>{book.name}</li>
          ))}
        </ul>
      </>
    )
}

export default BookList;

When i run the code above, we first get Loading... in DOM which is then updated to list containing query data (once it arrives). But how does react know to re-render my component once data is received from query.

Are these data, loading and error properties mapped to component props and they are updating? If so, why doesn't chrome dev tools show any props for this BookList component?

enter image description here

Can someone explain how is this useQuery custom hook working here?


Solution

  • A good way of figuring out (roughly) what is happening in useQuery is to consider how you'd do it yourself, e.g.

    const MyComponent = () => {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(false);
      const [error, setError] = useState(null);
    
      useEffect(async () => {
        try {
          setLoading(true);
          const data = await GraphQL.request(getBookQuery);
          setData(data);
        } catch (ex) {
          setError(ex);
        } finally {
          setLoading(false);
        }
      }, []);
    
      if(loading) return <p>Loading....</p>
      if(error) return <p>Ops! Something went wrong</p>
    
      return (
        <>
          <ul>
            {data.books.map(book => (
              <li key={book.name}>{book.name}</li>
            ))}
          </ul>
        </>
      );
    };
    

    In the above you can see your component has state (not props) data, loading and error which causes your component to re-render.

    You can then imagine this logic was wrapped in your own useQuery hook:

    const useQuery = (query, variables) => {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(false);
      const [error, setError] = useState(null);
    
      useEffect(async () => {
        try {
          setLoading(true);
          const data = await GraphQL.request(query, variables);
          setData(data);
        } catch (ex) {
          setError(ex);
        } finally {
          setLoading(false);
        }
      }, []);
    
      return { data, loading, error };
    }
    
    const MyComponent = () => {
      const { data, loading, error } = useQuery(getBookQuery);
    
      if(loading) return <p>Loading....</p>
      if(error) return <p>Ops! Something went wrong</p>
    
      return (
        <>
          <ul>
            {data.books.map(book => (
              <li key={book.name}>{book.name}</li>
            ))}
          </ul>
        </>
      );
    };
    

    So ultimately your component is re-rendering because it does have data, loading and error held in MyComponent state, it's just abstracted away.