apollographql / apollo-client

:rocket:  A fully-featured, production ready caching GraphQL client for every UI framework and GraphQL server.
https://apollographql.com/client
MIT License
19.38k stars 2.66k forks source link

Apollo Client 3 | React.StrictMode | @client @export field causes re-renders when same @export field is also queried by another component #6634

Open sheaosaurus opened 4 years ago

sheaosaurus commented 4 years ago

My team and I are trying to migrate our filter state from Redux to Apollo Local State. This filter state needs to be added as a variable to the server queries, and we were hoping to use @export to achieve this.

In our app, on mount, the following queries need to be resolved:

  1. Two or more separate filter components query their state from the cache using queries:
    
    const GET_POKEMON_NAME_FILTER = gql`
    query {
    pokemonName @client
    }
    `;`

const GET_POKEMON_TYPE_FILTER = gql query { pokemonType @client } `;

2. Any number of data table or visual graphics to query the server with the @export decorator in the query such as:

const GET_POKEMON = gql query($name: String, $pokemonType: String) { pokemonName @client @export(as: "name") pokemonType @client @export(as: "pokemonType") pokemon(name: $name, pokemonType: $pokemonType) { id number name } } ;


-----------------------  -----------------------  -----------------------

**Intended outcome:**

Given my understanding of @export, on mount, I expect the following to happen:

1. Either the filter component or the data component is rendered first
2. The @client field `pokemonName` is loaded in the cache already via cache.writeQuery (in our production app, this may not be possible other server request needing to be made before the defaults can be set)
3. The filter component reads from the cache using `useQuery`, grabs the pokemonName and renders twice to show loading as true then the return data
4. The data component runs the @export, resolves the pokemonName field, and runs the server request
5. The data is returned to the component once after the server request is complete with minimal rerenders

-----------------------  -----------------------  -----------------------

**Actual outcome:**
1. The filter component renders 6 times (via a console.count) with loading equal to true four times, and false twice. The data for this component is defined with the correct default value all six times.

2. The data component renders 8 times. The last two times it renders, the data object contains the correctly returned data from the server. 

3. The logic in the return and children component is run again with the same data in both components, unless React.memo is used to block the rerender. (If you place a React.memo'd component in the render function of the data component, it only renders once. This shows that the returned objects are indeed equal).

-----------------------  -----------------------  -----------------------

**How to reproduce the issue:**
Please see codesandbox link below with a very basic implementation of our use-case that also has the same re-rendering issue detailed above.

CodeSandbox: [https://codesandbox.io/s/apollo-local-state-export-fields-3cd46?file=/src/App.js](https://codesandbox.io/s/apollo-local-state-export-fields-3cd46?file=/src/App.js)

In the sandbox, I query a pokemon API in a data component using a pokemon name variable. A sibling filter component also needs to query this variable from local state as well.

- There are commented out console log/count statements in the data and filter components to show the amount of rerenders and what data is present on each render.
- I also tested this using a local only field and the same outcome as below was observed.
- We are almost certain reactive variables are not a use-case here

-----------------------  -----------------------  -----------------------

It appears as if the hooks are updating their internal state whenever the components rerender or the hook receives an update to one of its state values (ie loading, data etc).

The downstream consequence of this seems to be that when the data component changes, it toggles its loading state, causing the filter component to rerender as well even though the filter data has not changed.

 Given this multiple rendering, I wanted to ensure that I was not trying to extend @export beyond its capabilities or if the intended outcome is correct and this is a bug.

Thank you for any assistance.
-----------------------  -----------------------  -----------------------

**Versions**

`  System:
    OS: macOS 10.15.4
  Binaries:
    Node: 12.16.1 - /usr/local/bin/node
    Yarn: 1.22.4 - ~/.yarn/bin/yarn
    npm: 6.13.4 - /usr/local/bin/npm
  Browsers:
    Chrome: 84.0.4147.89
    Safari: 13.1
  npmPackages:
    @apollo/client: ^3.0.1 => 3.0.1`
sheaosaurus commented 4 years ago

To update this ticket, my team and I did a debugging session today and found that the cause for the re-rendering was React.StrictMode.

A brief background for anyone reading this who is not aware, React.StrictMode, which runs in development mode only, intentionally double renders the application to check for legacy code, unwanted side-effects, etc. React.StrictMode Docs

StrictMode Enabled: Our Data component renders 6-8 times with the correct data, as described in the actual outcome in the OP.

StrictMode Not-Enabled: In development mode, the Data component renders 2 times with no data, while the filter component renders twice and correctly receives its filter.

The high level implications of this are that any code we tested and expect to work in dev will break in production.


Updated Intended Outcome

With React.StrictMode not enabled, I expect the data returned from the server to be on the data object in the component. From the picture below, the data is returned from the server and placed on the Root.Query object.

Screen Shot 2020-07-23 at 5 39 56 PM

How to Reproduce

Please see updated Codesandbox with React.StrictMode removed: https://codesandbox.io/s/apollo-local-state-export-fields-strict-mode-disabled-rqmn1?file=/src/App.js

benknight commented 3 years ago

I just discovered this quirk today as well. Basically the double-rendering causes Apollo to create duplicate watched queries, so over time while using the application, the number of watched queries grows unbounded, and if you're using something like polling for example, because of the duplicate queries the server will be polled increasingly more frequently. I thought this was a bug in my code but realized it's just affecting development mode. Anyway long story short I've stopped using React.StrictMode so that Apollo queries behave similarly in dev and prod environments.