Search code examples
reactjsnext.jsaxiosreact-querytanstackreact-query

Is there a way to dynamically alter react-query query?


I have a next.js project with react-query. Basically what I need to do is display all the countries from the API on the homepage, but there's also an input to search by name, and a select to search by region. How can i change what query to call based on that?

I thought of saving the data to a useState and then displaying it but that seems like a waste of react-query's potential. Here's how I'm organizing my code:

countries.service.ts

import { Regions } from '@/queries/types';
import axios from 'axios';

export class CountriesService {
  static client = axios.create({
    baseURL: `${process.env.NEXT_PUBLIC_API_URL}`,
  });

  static async getAllCountries() {
    const response = await CountriesService.client.get('/all');

    return response.data;
  }

  static async getCountryByName(name: string) {
    const response = await CountriesService.client.get(`/name/${name}`);

    return response.data;
  }

  static async getCountryByRegion(region: Regions) {
    const response = await CountriesService.client.get(`/region/${region}`);

    return response.data;
  }
}

countries.query.ts

import { useQuery } from '@tanstack/react-query';

import { CountriesService } from '@/services/countries.service';
import { Regions } from './types';

export class CountriesQuery {
  static getAllCountries() {
    return useQuery({
      queryKey: ['get-all-countries'],
      queryFn: async () => {
        const data = await CountriesService.getAllCountries();

        return data;
      },
    });
  }

  static getCountryByName({ name }: { name: string }) {
    return useQuery({
      queryKey: ['get-country-by-name'],
      queryFn: async () => {        
        const data = await CountriesService.getCountryByName(name);

        return data;
      },
    });
  }

  static getCountryByRegion({ region }: { region: Regions }) {
    return useQuery({
      queryKey: ['get-country-by-region'],
      queryFn: async () => {        
        const data = await CountriesService.getCountryByName(region);

        return data;
      },
    });
  }
}

page.tsx

'use client';

import { CountryCard } from "@/components/country-card";
import { CountriesQuery } from "@/queries/countries.query";
import { Country } from "@/queries/types";
import { MagnifyingGlass } from "@phosphor-icons/react";
import { useState } from "react";

export default function Home() {
  const [searchName, setSearchName] = useState('');

  const { data, isLoading} = CountriesQuery.getAllCountries();

  const handleSearchByName = async (e: React.ChangeEvent<HTMLInputElement>) => {
    setSearchName(e.target.value);
  };

  if (isLoading) {
    //make skeleton
    return;
  };

  return (
    <main>
      <div>
        <input onChange={handleSearchByName} placeholder="Search for a country..." />
        <button><MagnifyingGlass weight="bold" /></button>
      </div>
      <div>
        {data.map((country: Country) => (
          <div key={country.name.common} >
            <CountryCard
              flagSrc={country.flags.png}
              name={country.name.common}
              population={country.population}
              region={country.region}
              capital={country.capital}
            />
          </div>
        ))}
      </div>
    </main>
  )
}

Solution

  • You've over-specified your custom query hooks (and also violated the rules of hooks).

    The correct way to implement this as a custom hook would be something like:

    export const useCountryQuery({ name, region, ...options } = {}) {
      return useQuery({
        queryKey: ['get-country', {name, region}]}
        queryFn: () => {
            if(name) {
              return CountriesService.getCountryByName(name);
            }
            if(region) {
              return CountriesService.getCountryByRegion(region);
            }
            return CountriesService.getAllCountries();
        },
        ...options
      })
    }
    

    To be used like:

    const [searchName, setSearchName] = useState('');
    
    const { data, isLoading } = useCountryQuery({name: searchName});
    
    const handleSearchByName = async (e: React.ChangeEvent<HTMLInputElement>) => {
        setSearchName(e.target.value);
    };
    // ...