The magic of react-query and supabase
Tags: react, reactquery, supabase, postgres
Published: 2021-06-28 15:06:09

---
title: The magic of react-query and supabase
published: true
description:
tags: react, reactquery, supabase, postgres
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/htnqku3iyeee8h85smxr.png
---

It's been a while since I wrote my last article on state management in React using Context. Here's the [link](https://dev.to/ankitjey/ciao-redux-using-react-hooks-and-context-effectively-398j) for anyone who wants to give it a read. And using custom hooks is still the primary way for me for state management and I have been recommending that to people as well.

In the previous post I had mentioned about UI state(theme, ux state) vs Server state(fetched data). I want to follow up on the sequel article I had promised. Let's get into it.

## What we are going to build
Let's not make yet another todo list. I think having some real world data will help understand things better. For this part we are going to make an app, where you can search movies from the TMDB api, add it to your profile as recommendation.

## What we are going to use
- [NextJS](https://nextjs.org/) - I, by default use NextJS for any react application I build nowadays over CRA.
- [react-query](https://react-query.tanstack.com/) - Data fetching/caching tool, going to help us with our "global/server state problems"
- [supabase](supabase.io/) - Supabase is something I have fallen in love with. It is an open source alternative to firebase(auth, database, storage) but the best part is it's Postgres. This will serve entirely as our backend. You will see how.
- [tailwindcss](tailwindcss.com/) - For styling our app.

Gotta say, all of these have the best developer experience you could ask for.

Let's get started.

## Setting up the client
First we need to [create the next app](https://nextjs.org/docs/getting-started) and [setup tailwind in it](https://tailwindcss.com/docs/guides/nextjs). 

## Setting up the backend(supabase)
Login into supabase and create a project. By default supabase provides you with [auth](https://supabase.io/auth). In this tutorial I won't be going all out on auth(will just do the login). After you create databases, all of them are accessible through the supabase client using an anon key that you get when you create a project. This is also where the best part of their auth architecture comes into place. All of the data by default are accessible to anyone using the anon key. But you can use row level policies on each table to achieve role/auth based authorization.

Let's first create a few tables using the inbuilt SQL editor in the dashboard, based on what we are trying to build.
```sql
CREATE TABLE users (
  id uuid references auth.users PRIMARY KEY,
  name text,
  username text unique
);

CREATE TABLE movies (
  movie_id integer PRIMARY KEY,
  title text,
  poster_path text,
  overview text,
  release_date date
);

CREATE TABLE recommendations (
   id uuid NOT NULL DEFAULT extensions.uuid_generate_v4(),
   primary key(id),
   user_id uuid,
   constraint user_id foreign key(user_id) references users(id),
   movie_id integer,
   constraint movie_id foreign key(movie_id) references movies(movie_id)
);

CREATE UNIQUE INDEX "user_id_movie_id" on recommendations using BTREE ("movie_id", "user_id");
```
You can create all the tables and relationships using the UI too if you want but you have both the options.
After running this the tables will be created for you. Let's see how our schema looks like using this [schema visualizer](https://supabase-schema.vercel.app/).
![alt text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hwr0ei4430w4ytq7yi1p.png)
 

### Initializing the client
Let's install the client.
```
yarn add @supabase/supabase-js
```
Create a file called app/supabase.ts and initialize the client.

```ts
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY);

export default supabase;
```
Make sure you copy over the project URL and anon key from your dashboard and paste it in `.env.local` file.

Before we go further let's setup `react-query` as well.

> React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your React applications a breeze.

### Setting up React Query
Install the package using
```
yarn add react-query
```
and add the following to your `_app.js`.

```tsx
...
imports
...

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 0
    }
  }
})

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <QueryClientProvider client={queryClient}>
      <Component {...pageProps} />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}
export default MyApp

```
React query has default retry of 3 times for queries, you can set your custom ones. We have set it to 0. We are also using the [devtools](https://react-query.tanstack.com/devtools) which is an awesome tool and helps us view queries and states easily.

Let's clarify a few things before going into this, `react-query` is data fetching and tool you can use anyway you like. A few people confuse this with Apollo Client, but Apollo Client is for GraphQL. React Query agnostic to what you are using to fetch data and just deals with promises. Which means you can deal with REST, GraphQL API, file system request as long as a promise is returned.

With React Query, queries are when you are fetching data from the server and mutations when you are changing data on the server.

### Signup
In signup we would be using supabase auth to signup and also create a user in the database with additional details. 

Create a page in `pages/auth/signup.tsx`, for the signup form
```tsx
import { useRouter } from "next/router"
import { useState } from "react"
import Loader from "../../components/ui/loader"

export default function Signup() {
  const router = useRouter()
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [name, setName] = useState('')
  const [username, setUsername] = useState('')

  return (
    <div className="min-h-screen grid place-items-center text-xl">
      <div className="w-2/3 lg:w-1/3 shadow-lg flex flex-col items-center">
      <h1 className="text-4xl font-semibold">Sign up</h1>
      <div className="mt-8 w-full lg:w-auto px-4">
          <p>Name</p>
          <input 
            type="text" 
            className="h-8 focus:outline-none shadow-sm border p-4 rounded mt-2 w-full lg:w-auto"
            onChange={e => setName(e.target.value)}
          />
        </div>
        <div className="mt-8 w-full lg:w-auto px-4">
          <p>Email</p>
          <input 
            type="text" 
            className="h-8 focus:outline-none shadow-sm border p-4 rounded mt-2 w-full lg:w-auto"
            onChange={e => setEmail(e.target.value)}
          />
        </div>
        <div className="mt-8 w-full lg:w-auto px-4">
          <p>Password</p>
          <input 
            className="h-8 focus:outline-none shadow-sm border p-4 rounded mt-2 w-full lg:w-auto"
            type="password"
            onChange={e => setPassword(e.target.value)}
          />
        </div>
        <div className="my-8 w-full lg:w-auto px-4">
          <p>Username</p>
          <input 
            type="text" 
            className="h-8 focus:outline-none shadow-sm border p-4 rounded mt-2 w-full lg:w-auto"
            onChange={e => setUsername(e.target.value)}
          />
        </div>
        <div className="mb-8 w-1/5">
          <button 
            className="bg-blue-500 text-white px-8 py-2 rounded w-full"
          >
            <span>Sign up</span>
          </button>
        </div>
      </div>
    </div>
  )
}
```

Let's create a custom hook in `hooks/useCreateUser.ts`
> We can always have the fetching/mutating inside the component but having separate custom hooks gives rise to cleaner code.

```ts
import { useMutation, useQueryClient } from "react-query"
import supabase from "../app/supabase"

interface User {
  name: string;
  email: string;
  username: string;
  password: string;
}

const createUser = async (user: User) => {
  // Check if username exists
  const { data: userWithUsername } = await supabase
    .from('users')
    .select('*')
    .eq('username', user.username)
    .single()

  if(userWithUsername) {
    throw new Error('User with username exists')
  }

  const { data, error: signUpError } = await supabase.auth.signUp({
    email: user.email,
    password: user.password
  })

  if(signUpError) {
    throw signUpError
  }

  return data
}

export default function useCreateUser(user: User) {
  return useMutation(() => createUser(user), {
    onSuccess: async(data) => {
      const { data: insertData, error: insertError } = await supabase
        .from('users')
        .insert({
          name: user.name,
          username: user.username,
          id: data.user.id
        })

      if(insertError) {
        throw insertError
      }

      return insertData
    }
  })
}
```
Let's go through the above code. 

First we have the method the create the user. In there we first check whether an user with the username exists and if it does we throw an error. So a thing to notice here is that the supabase client by default doesn't throw an error, instead returns it in the return object. Then we use `supabase.auth.signUp()` method with email and password. We have disabled, the email verification in supabase auth dashboard for this tutorial. If it succeeds we return the data we get back.

Next we have the default export which uses the `useMutation` hook from react query. We pass in the function we created above. Also since we also want to insert a user in our users table, we have `onSuccess` [side effect](https://react-query.tanstack.com/guides/mutations#mutation-side-effects) in options which gets the data returned by the `createUser` method. Here we use `supabase.from` to build a insert query and we use the user id returned from the signup success.

Perfect, now we add the logic in `pages/auth/signup`
```tsx
...
import useCreateUser from "../../hooks/useCreateUser"

export default function Signup() {
...
  const createUserMutation = useCreateUser({
    email,
    password,
    name,
    username
  })

  if(createUserMutation.isSuccess) {
    router.push("/")
  }

...

{createUserMutation.isError && <p className="text-sm mb-8 text-red-500">{createUserMutation.error.message}</p>}

...

<button 
    className="bg-blue-500 text-white px-8 py-2 rounded w-full"
    onClick={() => createUserMutation.mutate()}
    >
            {createUserMutation.isLoading? 
              <span>
                <Loader 
                  height={30}
                  width={30}  
                />
              </span> :
            <span>Sign up</span>
            }
    </button>
```
We import the custom hook and define it in our component. We add an onclick action on the button which triggers the mutation. We also use the `isLoading`, `isError`, `error` for displaying. We use the `isSuccess` to route the user to the home page.

Now on entering the details and clicking signup a user should be created, and you should be redirected to the signup page.

### Login
Let's quickly add the login page as well.

Let's create a new page at `auth/login` route and add some simple ui.
```tsx
export default function Login() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  return (
    <div className="min-h-screen grid place-items-center text-xl">
      <div className="w-2/3 lg:w-1/3 shadow-lg flex flex-col items-center">
        <div className="mt-8 w-full lg:w-auto px-4">
          <p>Email</p>
          <input 
            type="text" 
            onChange={e => setEmail(e.target.value)}
            className="h-8 focus:outline-none shadow-sm border p-4 rounded mt-2 w-full lg:w-auto"
          />
        </div>
        <div className="my-8 w-full lg:w-auto px-4">
          <p>Password</p>
          <input 
            className="h-8 focus:outline-none shadow-sm border p-4 rounded mt-2 w-full lg:w-auto"
            type="password"
            onChange={e => setPassword(e.target.value)}
          />
        </div>
        <div className="mb-8">
          <button className="bg-blue-500 text-white px-8 py-2 rounded">Login</button>
        </div>
      </div>
    </div>
  )
}
```
Create a similar hook called `hooks/useLogin.ts`
```tsx
import { useMutation } from 'react-query'
import supabase from '../app/supabase'

const login = async ({email, password}) => {
  const { data, error } = await supabase.auth.signIn({
    email, 
    password
  })

  if(error) {
    throw new Error(error.message)
  }

  return data
}

export default function useLogin({ email, password }) {
  return useMutation('login', () => login({email, password}))
}
```
And similarly in `pages/auth/login.tsx`
```tsx
...
const loginMutation = useLogin({email, password})

  if(loginMutation.isSuccess) {
    router.push('/')
  }
...
...

{loginMutation.isError && <p className="text-sm mb-8 text-red-500">{loginMutation.error.message}</p>}
...
<button 
    className="bg-blue-500 text-white px-8 py-2 rounded w-full"
    onClick={() => loginMutation.mutate()}
  >
      {loginMutation.isLoading? 
        <span>
          <Loader 
            height={30}
            width={30}  
          />
        </span> :
        <span>Login</span>
      }
   </button>
```
It's pretty similar to signup, we call the `supabase.auth.signIn` method and redirect the user if the mutation is successful.

Now if you enter your credentials, login should work.

### Authenticated Pages
Now when the user logs in we want to fetch the user details, name and username in our case which will be available to the entire app. Let's create a hook for that.

Create a file in `hooks/useUser.ts`
```tsx
import { useQuery } from 'react-query'
import supabase from '../app/supabase'

const getUser = async ({userId}) => {
  const { data, error } = await supabase
    .from('users')
    .select()
    .eq('id', userId)
    .single()

  if(error) {
    throw new Error(error.message)
  }

  if(!data) {
    throw new Error("User not found")
  }

  return data
}

export default function useUser() {
  const user = supabase.auth.user()
  return useQuery('user', () => getUser(user?.id))
}
```
The useQuery hook needs a unique key as the first parameter. > At its core, React Query manages query caching for you based on query keys. Query keys can be as simple as a string, or as complex as an array of many strings and nested objects. As long as the query key is serializable, and unique to the query's data, you can use it! Read more [here](https://react-query.tanstack.com/guides/query-keys).

We define a `getUser` method which uses the supabase client query builder. This is equivalent to
```sql
SELECT * FROM users where id = <userId>
```
In the default export, we use the `supabase.auth.user()` method which returns the user if session exists. Note the `user?id` in the `getUser` method call, this is because the `auth.user` method can initially return null and eventually resolves to a value.

Now we want to make our home page authenticated. So when a user doesn't have a session, he will be redirected to the login page.

To do that let's create a file in `components/Protected.tsx`
```tsx
import Loader from "./ui/loader"
import { useRouter } from 'next/router'
import useUser from "../hooks/useUser"

export default function ProtectedWrapper({children}) {
  const router = useRouter()
  const { isLoading, isError } = useUser()
  if(isLoading) {
    return (
      <div className="h-screen grid place-items-center">
        <Loader height={200} width={200}/>
      </div>
    )
  }

  if(isError) {
    router.push('/auth/login')
    return (
      <div className="h-screen grid place-items-center">
        <Loader height={200} width={200}/>
      </div>
    )
  }

  return (
    <div>
      {children}
    </div>
  )
}
```
This is a wrapper component which basically checks for the session and redirects if it's not there. Let's see how it happens. So we are using the `useUser` we defined earlier and destructuring `isLoading` and `isError` from the it. If it's loading, we display a loader and if the query errors we redirect the user.

The `isLoading` state happens when the query is being fetched for the first time, likely during component mount for the first time/window reload.

The `isError` state is when the `useUser` query errors. This is the beauty of react query. If the session doesn't exist, the `supabase.auth.user()` will never resolve to a value and the `getUser` call will throw an error.

Also when the value returned from `supabase.auth.user` changes from `null` to user, the query is automatically refetched.

Now let's use this `ProtectedWrapper` inside our index page.

```tsx
...
import ProtectedWrapper from "../components/Protected"

export default function Home() {
  return (
    <ProtectedWrapper>
      ...
    </ProtectedWrapper>
  )
}
```
Let's see it in action.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zd8zhc1b6xoya9dji42z.gif)
This one is when there is no session.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yui8qd4ue2qxy1tylk7c.gif)
This one is where browser session exists.

Awesome, we can now use this wrapper in pages which we want to be authenticated.

### Displaying the user

Let's create a Navbar component
```tsx
import Link from 'next/link'
import Loader from "../ui/loader";
import { useRouter } from "next/router";

export default function Navbar() {
  return (
    <div className="flex items-center justify-around py-6 bg-blue-500 text-white shadow">
      <Link href="/">
        <div className="text-2xl">
          Home
        </div>
      </Link>
      <div className="text-xl flex items-center space-x-4">
        <div>
          <Link href="/search ">
            Search
          </Link>
        </div>
        <div>
          Username
        </div>
        <div
          className="cursor-pointer"
        >
          {/* Logout feather icon */}
          <svg 
            xmlns="http://www.w3.org/2000/svg" 
            width="24" 
            height="24" 
            viewBox="0 0 24 24" 
            fill="none" 
            stroke="currentColor" 
            strokeWidth="2" 
            strokeLinecap="round" 
            strokeLinejoin="round" 
            className="feather feather-log-out"
          >
            <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line>
          </svg>
        </div>
      </div>
    </div>
  )
}
```

Now let's say we want to display the username in our Navbar, we don't have to do anything but reuse the `useUser` query again in the Navbar component. React query by default caches all queries for 5 mins(can be changed), after which the query is refetched. Here's how.
```tsx
...
import useUser from "../../hooks/useUser"

export default function Navbar() {
  const { data, isLoading } = useUser({userId: user?.id})
  ...
      <div>
        {isLoading ? 
          <span>
            <Loader 
              height={30}
              width={30}
            />
          </span>
        : data?.username}
      </div>
    ...
```
A few things that `react-query` takes care for us here
- We didn't have to add any logic to share the state, we can just use the data from the hook
- We get, the state object in navbar as well which we use to display a loading indication incase the user is being fetched

No declaring of many initial states, and dispatching of actions. :3

### Log out
Let's also add the log out logic in the navbar. You know the script, create a hook and use the hook.
```ts
import { useMutation, useQueryClient } from "react-query"
import supabase from "../app/supabase"


const logout = async () => {
  const { error } = await supabase.auth.signOut()

  if(error) {
    throw error
  }
}

export default function useLogOut() {
  const queryClient = useQueryClient()
  return useMutation(() => logout(), {
    onSuccess: () => {
      queryClient.removeQueries()
    }
  })
}
```
We use the `supabase.auth.signOut` which destroys the session and logs the user out.
A thing to notice here is since our app uses queries to display data and not any kind of store, we need to remove the queries once a user logs out. To do that we use the [queryClient](https://react-query.tanstack.com/reference/QueryClient) from the `useQueryClient` hook and on the success side effect we remove all the queries using `queryClient.removeQueries` method.

```tsx
...
import useLogOut from "../../hooks/useLogOut";
import { useRouter } from "next/router";

...

export default function Navbar() {
  const logoutMutation = useLogOut()
  const router = useRouter()

  if(logoutMutation.isSuccess) {
    router.push('/auth/login')
  }

  ...

  <div
          className="cursor-pointer"
          onClick={() => logoutMutation.mutate()}
        >
          <svg 
            ...
          </svg>
        </div>
```
Done, clicking the logout button now destroys the session and redirects to the login page.

### Searching for movies
We know the pattern now, let's create a hook for searching movies.
Create a file in `hooks/useMovies.ts`
```tsx
import { useQuery } from 'react-query'

const searchMovies = async (query) => {
  const response = await fetch(`https://api.themoviedb.org/3/search/movie?api_key=${process.env.NEXT_PUBLIC_TMDB_API_KEY}&query=${query}&language=en-US&page=1`)

  if(!response.ok) {
    throw new Error('Error searching movies')
  }

  return response.json()
}

export default function useMovies({ query }) {
  return useQuery('movies', () => searchMovies(query), {
    enabled: false
  })
}
```
The `enabled: false` here means the query doesn't run automatically and has to be manually triggered using `refetch`. More [here](https://react-query.tanstack.com/guides/disabling-queries)

Create a page called `search.tsx`
```tsx
import Navbar from "../components/layouts/navbar"
import Search from "../components/search"
import ProtectedWrapper from "../components/Protected"

export default function Home() {
  return (
    <ProtectedWrapper>
      <div className="min-h-screen">
        <Navbar />
        <div className="container mx-auto">
          <Search />
        </div>
      </div>
    </ProtectedWrapper>
  )
}
```

And the Search component in `components/search/index.tsx`
```tsx
import { useState } from 'react'
import useMovies from '../../hooks/useMovies'
import SearchResultItem from './SearchResultItem'
import Loader from '../ui/loader'

export default function Search() {
  const [query, setQuery] = useState('')
  const { refetch, isFetching, data, isSuccess, isIdle } = useMovies({query})
  return (
    <div className="mt-20 text-xl flex flex-col items-center">
      <div className="flex">
        <input 
          className="border shadow px-8 py-2 rounded focus:outline-none" 
          onChange={e => setQuery(e.target.value)}  
        />
        <button 
          className="bg-blue-500 py-2 px-4 shadow rounded text-white w-32"
          onClick={() => refetch()}
        >
          {
            isFetching ? 
            <span>
              <Loader 
                height={30}
                width={30}
              />
            </span>: 
            `Search`
          }
        </button>
      </div>
      <div className="mt-10">
        {isSuccess  && 
          <div className="grid place-items-center">
            {data
              ?.results
              .sort((a, b) => b.popularity - a.popularity)
              .map(
                (item, index) => 
                <SearchResultItem 
                  title={item.title} 
                  overview={item.overview} 
                  key={index}
                  poster_path={item.poster_path}
                  release_date={item.release_date}
                /> 
              )
            }
          </div>
        }
      </div>
        {isSuccess 
          && !data?.results.length
          &&   
          <div className="mt-10">
            <p>No results found</p>
          </div>
        }
      {isIdle && <div className="mt-10">Search for a movie</div>}
    </div>
  )
}
```
And the search item component
```tsx
import dayjs from 'dayjs'

export default function SearchResultItem({title, overview, poster_path, release_date}) {
  return (
    <div className="flex w-2/3 mt-4 shadow rounded py-2">
      <div className="h-30 w-1/4 grid place-items-center flex-none">
       <img src={`https://www.themoviedb.org/t/p/w94_and_h141_bestv2${poster_path}`} alt="poster" height="150" width="150" />
      </div>
      <div className="px-4 flex flex-col justify-around">  
        <p className="text-2xl">{title}</p>
        <p className="text-base">{overview.slice(0, 200)}...</p>
        <p className="text-base">{dayjs(release_date).format('YYYY')}</p>
        <button className="w-20 px-6 py-2 text-base bg-blue-500 text-white rounded">Add</button>
      </div>
    </div>
  )
}
```

Now we can search for a movie and are displaying it in a list. One thing you will notice that even if you change pages and come back to the search page, the movie results if you had searched would have been cached and are shown. Woohoo.

### Adding a movie to your recommendation
Let's create another hook for that.
In a file `hooks/useAddMovie.ts`
```tsx
import { useMutation } from "react-query"
import supabase from "../app/supabase"

interface Movie {
  movie_id: number;
  title: string;
  overview: string;
  poster_path: string;
  release_date: string;
}

const addMovie = async (movie: Movie, user_id: string) => {
  const { error } = await supabase
  .from('movies')
  .upsert(movie)
    .single()
    
    if(error) {
      throw error
    }
    
    const { data, error: err } = await supabase
    .from('recommendations')
    .upsert({movie_id: movie.movie_id, user_id}, {
      onConflict: 'user_id, movie_id'
    })
    .single()
    
    if(err) {
      throw err
  }
  
  return data
}

export default function useAddMovie(movie: Movie) {
  const user = supabase.auth.user()
  return useMutation(() => addMovie(movie, user?.id))
}
```
Note that we are using `upsert` in both the calls, one to save the movie details so a duplicate movie isn't added and second to prevent a duplicate entry in recommendation(we have the `onConflict` clause to satisfy the unique index constraint). Also we are using `supabase.auth.user()` to pass in the user id, for the second method.

Then in `components/search/SearchResultItem.tsx`
```tsx
...
imports
...

export default function SearchResultItem({id, title, overview, poster_path, release_date}) {
  const addMovie = useAddMovie({
      movie_id: id, 
      title, 
      overview, 
      poster_path, 
      release_date
    })

  ...

        <button 
          className="w-32 px-6 py-2 text-base bg-blue-500 text-white rounded"
          onClick={() => addMovie.mutate()}
        >
          {addMovie.isLoading ? 
            <span>
              <Loader 
                height={25}
                width={25}
              />
            </span>: 
            `Add`}
        </button>
 ...
```
Awesome now we can add a movie to our list. The last thing remaining is to display them in the home screen.

### Displaying your recommendations
Create a file in `hooks/useRecommendations.ts`
```tsx
import { useQuery } from 'react-query'
import supabase from '../app/supabase'

const fetchRecommendations = async (user_id) => {
  const { data, error } = await supabase
    .from('recommendation')
    .select(`
      movie (
        *
      )
    `)
    .eq('user_id', user_id)

  if(error) {
    throw new Error(error.message)
  }

  return data
}

export default function useRecommendations() {
  const user = supabase.auth.user()
  return useQuery('recommendations', () => fetchRecommendations(user?.id))
}
```
Here we are fetching from the foreign table movie using the movie id foreign key and matching by the user id.

Let's update our `components/recommendations/index.tsx`
```tsx
import Link from 'next/link'
import useRecommendations from '../../hooks/useRecommendations'
import MovieCard from './MovieCard'
import Loader from '../ui/loader'

export default function Recommendations() {
  const { data, isSuccess, isLoading } = useRecommendations()
  if(isLoading) {
    return (
      <div className="h-screen grid place-items-center">
        <Loader height={200} width={200} />
      </div>
    )
  }
  return (
    <div>
      <h2 className="text-3xl my-4">Your recommendations</h2>
      <hr />
      {isSuccess && !data.length && <div className="mt-20 text-xl grid place-items-center">
        <p>You have no recommendations yet.</p>
        <p>
          <span className="cursor-pointer text-blue-500"><Link href="/search">Search</Link></span>
          <span>{` `}for movies and add them to your recommendations.</span>
        </p>
      </div>}
      {
        isSuccess &&
        <div className="grid grid-cols-3 gap-x-4 gap-y-4">
          {data.map(({movie: {
              movie_id, 
              title, 
              overview,
              poster_path,
              release_date
            } }) => (
            <MovieCard 
              key={movie_id}
              title={title}
              poster_path={poster_path}
            />
          ))}
        </div> 
      }
    </div>
  )
}
```
And `components/recommendations/MovieCard.tsx`
```tsx
export default function MovieCard({title, poster_path}) {
  return (
    <div className="grid place-items-center shadow rounded py-4">
      <img src={`https://www.themoviedb.org/t/p/w300_and_h450_bestv2${poster_path}`} />
      <p className="mt-4 text-2xl font-semibold">{title}</p>
    </div>
  )
}
```

Perfect, now when we load the home page we have a loader when the query is fetched. If you go into search and add a movie, you will see the home page will have fetched that automatically. That's because when move to a different page, the recommendations query becomes inactive and is automatically fetched again on component mount. If you open devtools you will also notice that the `useUser` query is also being fetched multiple times(when we go to a new page)

>Stale queries are refetched automatically in the background when:
    New instances of the query mount
    The window is refocused
    The network is reconnected.
    The query is optionally configured with a refetch interval.

This behaviour is good but sometimes undesirable. Gladly we can configure it in query default options.

```tsx
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 0,
      refetchOnMount: false,
      refetchOnWindowFocus: false
    }
  }
})
```
We can also add this individually to a query. Now that we have disabled auto fetch on remount, we want to refetch the query when we add a movie from search page.

For this we can again use the [`queryClient`](https://react-query.tanstack.com/reference/QueryClient) from the `useQueryClient` hook. Here we want to use the `refetchQueries` method. If the query is currently being used in the same page, you can use `invalidateQueries` method which makes the stale and are refetched automatically. Since our use case is for a different page we will use `refetchQueries` instead.

In our `hooks/useAddMovie.ts` file
```ts
...
export default function useAddMovie(movie: Movie) {
  const queryClient = useQueryClient()
  const user = supabase.auth.user()
  return useMutation(() => addMovie(movie, user?.id), {
    onSuccess: () => {
      queryClient.refetchQueries('recommendations')
    }
  })
}
```
Now when you add a movie, the query is refetched automatically.

The end result
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ji5uw5ax96k78evbtm77.gif)

React query has so many features, it's impossible to cover them all in a go. You can play around with `react-query` with an application, even better if you refactor an existing one to `react-query`.

The code until this point is on [github](https://github.com/ankitjena/react-query-supabase/tree/ea1999796bc281f11c50d4c3954122a96e2dc29b)

That's it for this part. In the next part we will build upon this app and add lists, which you can create and add your recommendations into and more features. We will delve more into supabase (row level policies etc), and more react query features.

Thanks for reading up to this point. If you have any questions or doubts feel free to ask them in the comments. If you liked the post like and share it on twitter.

Documentation links
- NextJS - [https://nextjs.org/docs/getting-started](https://nextjs.org/docs/getting-started)
- React Query - [https://react-query.tanstack.com/overview](https://react-query.tanstack.com/overview)
- Supabase Auth - [https://supabase.io/docs/guides/auth](https://supabase.io/docs/guides/auth)
- Supabase Client - [https://supabase.io/docs/reference/javascript/supabase-client](https://supabase.io/docs/reference/javascript/supabase-client)
- Tailwind CSS - [https://tailwindcss.com/docs](https://tailwindcss.com/docs)