What is the best way to initiate data when a user logs in?

In my application users can add movies to their watchlist (a list on the applications database). I have everything working using React, GraphQL, Apollo Client, Apollo Server and Prisma. It’s a great stack in my opinion.

One thing I do have issues with is how I retrieve the movies connected to the user when the user logs in. I feel it’s a little bit spaghetti code and it leads to unwanted API calls.

I have a custom react hook that returns the list of movies from that user:

const returnMoviesFromUserHook = () => {
  const {error, loading, data} = useQuery(
      resolvers.queries.ReturnMoviesFromUser, {fetchPolicy: 'no-cache', variables: {userId: currentUserVar().id}});
  if (!loading && !error) {
    return data.moviesFromUser;
  }
};

In my login.tsx I have a line const movies = returnMoviesFromUserHook(); to retrieve all the movies when the login component is rendered. This is the first annoyance, this will also trigger a API call when the user is not logged in. And it triggers a API call if the user logs out (logic is in the same component).

Secondly I need a:

  useEffect(() => {
    moviesVar(movies);
  });

to set the movies returned from the custom hook to a reactive variable called moviesVar.

Then finally in my dashboard component I can do this:

const movies: IMovie[] = useReactiveVar(moviesVar);

So when the user logs in, the API is called and it returns a list of movies which get stored in the reactive moviesVar variable. If this variable changes it will cause a rerender of the dashboard component showing the list of movies.

But like I said, this has some nasty side-effects. I was wondering if people here have some ideas on how to improve this flow.

Hi! I may be missing something, but can’t you just use the useQuery hook in your dashboard component, and not on your login page (also removing the fetchPolicy so that the data is cached)? That way, no request is made when the user is not logged in and you don’t need a reactive variable.

The issue is that if I put the initialization logic in the dashboard component it would run each time I rerender the component. Which happens when a movie is added to the the users list. What happens is that on initialization the dashboard component does the request for the movies. The request returns an array of movies. Then why I add a movie, the response of that request contains all the movies of that user (including the newly added one). This response goes into the reactive variable to rerender the movies on the dashboard. But if I also request all the movies on that dashboard component I would get 2 requests where only 1 is needed, and an issue where I would need to populate 1 variable with 2 values.

const listOfMovies = useQuery (for initialization)
const listOfMovies = listOfMoviesVar (react on the changes on the reactive variable).

Here’s my suggestion:

  • Have the dashboard component retrieve the list of movies using useQuery (without a fetch policy, so that the response data is stored in the cache).
  • Simply use the response data from that query to render your list of movies. Don’t use a reactive variable.
  • When a mutation is made to add a new movie, your cache will be updated automatically if I understood you correctly and the mutation returns the user with the entire list of movies rather than just the new one.
  • Your component will be rerendered, but there will be no additional request to fetch the movies because all the necessary data is already in the cache. This is only true if all the movie fields requested in the query are also present in the mutation.

The addMovieToUser mutation doesn’t return the user object with the movies, but all the movies connected to the user. But I see your point. I need to look into caching!

In that case, you can do the following:

useMutation(mutation, {
    update: (cache, result) => {
        cache.modify({
            id: cache.identify(YOUR_USER_OBJECT),
            fields: {
                movies: (previous, { toReference }) => (
                    result.data.addMovieToUser.map((movie) => toReference(movie))
                ),
            },
        }):
    },
});

If you haven’t dabbled with the cache before, this may look super complicated, but each part of it is fairly straightfoward. update is called after a mutation request is done. It is passed an instance of the cache and the mutation result. cache.modify allows you to change a specific part of the cache. In this case, you would like to set a certain user’s movies to the mutation response. The id passed to cache.modify is the key of the cache object that you wish to modify. The key can be obtained by calling cache.identify on an object that was returned by a useQuery hook - here you need the user object, because that is what you want to modify in the cache. fields determines the fields that you wish to change on the cache object - in your scenario, the movies field needs to be changed. Each field is a modifier function that is passed two parameters: the previous value of that field in the cache and an object containing various helpers to interact with the cache. The modifier function is supposed to return the new value for the cache object, which is the list of movies returned by the mutation. Now here’s the least straightforward bit: Unfortunately you can’t just return result.data.addMovieToUser and be done with it because the Apollo cache is normalized. You don’t want to set the movies list to the actual movie objects but instead to references to those movies in the cache. That is accomplished by mapping over the movies and returning a reference for each movie using toReference.

And that’s it. Seems like a lot, but it is of great advantage to know how the cache works in order to create a clean application. If you wish to learn more about this, I would recommend this extensive caching guide I wrote a while ago. It should lead you through this complex topic and answer all the question you have.

1 Like

I’m not getting this part. What is id referring to, I’ve checked the docs:

  • In the id field, we use cache.identify to obtain the cache ID of the cached Post object we want to remove a comment from.

You say I need the user object, but my user object has no reference to any movies. I have a query that returns the movies connected to an user:

const currentUser = useReactiveVar(currentUserVar);

  const {error, loading, data: {moviesFromUser} = {}} = useQuery(resolvers.queries.ReturnMoviesFromUser, {
    variables: {userId: currentUser.id},
  });

  if (error) return (<div>Error...</div>);
  if (loading) return (<div>Loading...</div>);

  const [addUserToMovie] = useMutation(resolvers.mutations.AddUserToMovie, {
    update: (cache, result) => {
      console.log(cache);
      console.log(result);
      console.log(moviesFromUser);
      cache.modify({
        id: cache.identify(moviesFromUser),
        fields: {
          movies: (previous, {toReference}) => (
            result.data.addUserToMovie.map((movie) => toReference(movie))
          ),
        },
      });
    },
  });

  const addMovie = async (movie: IMovie) => {
      await addUserToMovie({variables: {
        ...movie,
        tmdb_id: movie.id,
      }});
    }
  };

I added a useQuery to return the movie array object I want to use. But this line of code result.data.addUserToMovie.map((movie) => toReference(movie)) is never reached.