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.39k stars 2.66k forks source link

Warning: Cannot update a component (`Movies`) while rendering a different component (`Login`). #8758

Open alucardu opened 3 years ago

alucardu commented 3 years ago

Intended outcome:

Trying to set a value in a reactive variable in one component (Login) and read the value in a different component (Movies)

// Login.js

const Login = () => {
 moviesVar([{name: 'Movie name'}])
 return (
   <div>Login</div>
 ) 
}
// Movies.js

const Movies = () => {
 const movies = useReactiveVar(moviesVar)
 return (
   <div>
     Movies: {movies.map((movie, index) => {
       return <p key={index}>{movie.name}</p>
     })}

  </div>
 ) 
}

The values are set/passed/read correctly, but a warning shows up.

Actual outcome:

image

How to reproduce the issue: Visit > https://codesandbox.io/s/sleepy-fire-im8ug?file=/src/App.js:216-221 On initial load the app works fine.

  1. change a text somewhere
  2. save the changes
  3. wait for app to reload
  4. check console

Versions

    "@apollo/client": "3.4.10",
    "graphql": "15.5.3",
    "react": "17.0.2",
    "react-dom": "17.0.2",
    "react-scripts": "4.0.0"
brainkim commented 3 years ago

@alucardu Thanks for opening an issue! The warning seems to have something to do with Code Sandbox and the hot reload code they’re using. I’m not too sure if this is a problem if the page is working as expected.

The warning:

Warning: Cannot update a component (`Movies`) while rendering a different component (`Login`). To locate the bad setState() call inside `Login`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
Login
div
App
ApolloProvider
alucardu commented 3 years ago

Hey @brainkim I have this same issue on my local app, except then it appears when the app loads (not on a reload). I'm also not sure if there's a problem since everything is working as expected, but I'm still getting the warning.

brainkim commented 3 years ago

@alucardu I’ll look into it! Sorry about the trouble! If you have a reproduction of it happening on load and not reload that would be super helpful!

alucardu commented 3 years ago

https://github.com/facebook/react/issues/22252#event-5261949382 perhaps this is something that could help?

brainkim commented 3 years ago

@alucardu Looking at this again, it looks like there is a reactiveVar update (moviesVar([{name: 'Movie name'}])) directly in the render function in Login.tsx. This is akin to calling setState() directly in the render function. Perhaps, if you move it to a useEffect() call, the warnings will go away?

IndigoLogic commented 2 years ago

I'm having a similar issue, though all of my calls to set my reactive variables are either in a useEffect() or inside a function, not directly in the render. For example:

export const AddressModal = ({
  addressModalOpen,
  authenticatedUserAddress,
  isAuthenticated,
  onClose,
  isTablet
}: ModalProps) => {
  const [localAddress, setLocalAddress] = useState('');
  const [deliveryInstructions, setDeliveryInstructions] = useState('');
  const [addressLabel, setAddressLabel] = useState('');
  const [isDefault, setIsDefault] = useState(false);
  const [mapVisible, setMapVisible] = useState(false);

  const [editCustomerAddress] = useMutation(CUSTOMER_ADD_ADDRESS, {
    refetchQueries: [{ query: CUSTOMER_ADDRESS_DATA }, { query: CUSTOMER_DATA }]
  });
...
const handleSave = async () => {
    const googleAddress: GoogleAddress = await getCoordinatesFromAddress(localAddress);
    setLocalState('deliveryAddress', googleAddress);
    const addressExists = await isExistingAddress(googleAddress, authenticatedUserAddress);
    if (!addressExists) {
      const address = {
        customerNotes: deliveryInstructions,
        isDefault: isDefault,
        streetAddress: googleAddress.streetAddress,
        streetAddress2: googleAddress.streetAddress2,
        city: googleAddress.city,
        state: googleAddress.state,
        zip: googleAddress.zip,
        label: addressLabel
      };

      try {
        editCustomerAddress({
          variables: {
            address: {
              ...address
            }
          }
        });
      } catch (error) {
        logger.error(error);
      }
      //   }
    }
    setMapVisible(false);
    onClose();
  };
  ...

The reactive var is being set by the setLocalState call on line 2 of the handleSave() function. setLocalState is a wrapper that sets the reactive var and persists the value in local storage for offline use and use by micro front-ends. Here is the wrapper:

  export const setLocalState = <T extends keyof typeof VARIABLES>(
  key: T,
  value: ReturnType<typeof VARIABLES[T]['var']>
) => {
  const reactiveVar: ReactiveVar<any> = VARIABLES[key].var;
  localStorage.setItem(key, getCleanValueForStorage(value));
  reactiveVar(value);
};

Where VARIABLES is an object of reactiveVariables, like this

export const VARIABLES = {
  deliveryAddress: {
    var: deliveryAddressReactiveVar
  },
};

And deliveryAddressReactiveVar is defined as

import { makeVar } from '@apollo/client';
import { Address } from '@/shared/src/types/address';

export const deliveryAddressReactiveVar = makeVar<Address>({});