ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • React Query
    카테고리 없음 2022. 7. 11. 19:33

    Introduction

    What?

    A library for fetching data in a React application

    Why?

    1. Since React is a UI library, there is no specific pattern for data fetching
    2. useEffect hook for data fetching and useState hook to maintain component state like loading, error or resulting data
    3. If the data is needed throughouut the app, we tend to use state management libraries
    4. Most of the state management libraries are good for working with client state. Ex: theme for the application / whether a modal is open
    5. State management libraries are not great for working with asynchronous or server state

    Client state vs Server state

    Client state

    • Persisted in your app memory and accessing or updating it is synchronous

    Server state

    • Persisted remotely and requires asynchronous APIs for fetching or updating
    • Has shared ownership
    • Data can be updated by someone else without your knowledge
    • UI data may not be in sync with the remote data
    • Challenging when you have to deal with caching, deduping multiple requests for the same data, updating stale data in the background, performance optimizations etc

    Content

    • Basic querires
    • Poll data
    • React query dev tools
    • Create reusable query hooks
    • Query by ID
    • Parallel queries
    • Dynamic queries
    • Dependent queries
    • Infinite & paginated queries
    • Update data using mutations
    • Invalidate queries
    • Optimistic updates
    • Axios Intercetor

    Pre-requisites

    • React Fundamentals
    • React Hooks

    Project Setup

    1. New react project using CRA
    2. Set up an API endpoint that serves mock data for use in our application
    3. Set up react router and a few routes in the application
    4. Fetch data the traditional way using useEffect and useState

    Traditional way fetching data

    src/components/SuperHeroes.page.tsx

    import { useEffect, useState } from 'react';
    import axios from 'axios';
    import { Hero } from '../typings/Heroes';
    
    const SuperHeroesPage = () => {
      const [isLoading, setIsLoading] = useState(true);
      const [data, setData] = useState<Hero[]>([]);
    
      useEffect(() => {
        axios.get('http://localhost:4000/superheroes').then((res) => {
          setData(res.data);
          setIsLoading(false);
        });
      }, []);
    
      if (isLoading) {
        return <h2>Loading...</h2>;
      }
    
      return (
        <>
          <h2>Super Heroes Page</h2>
          {data.map((hero) => {
            return <div key={hero.name}>{hero.name}</div>;
          })}
        </>
      );
    };
    
    export default SuperHeroesPage;

    Fetching data with useQuery

    Using QueryClientProvider and QueryClient

    src/App.tsx

    import { BrowserRouter as Router, Link, Route, Switch } from 'react-router-dom';
    import { QueryClientProvider, QueryClient } from 'react-query';
    import './App.css';
    import HomePage from './components/Home.page';
    import RQSuperHeroesPage from './components/RQSuperHeroes.page';
    import SuperHeroesPage from './components/SuperHeroes.page';
    
    const queryClient = new QueryClient();
    
    const App = () => {
      return (
        <QueryClientProvider client={queryClient}>
          <Router>
            <div>
              <nav>
                <ul>
                  <li>
                    <Link to="/">Home</Link>
                  </li>
                  <li>
                    <Link to="/super-heroes">Traditional Super Heroes</Link>
                  </li>
                  <li>
                    <Link to="/rq-super-heroes">RQ Super Heroes</Link>
                  </li>
                </ul>
              </nav>
              <Switch>
                <Route path="/super-heroes">
                  <SuperHeroesPage />
                </Route>
                <Route path="/rq-super-heroes">
                  <RQSuperHeroesPage />
                </Route>
                <Route path="/">
                  <HomePage />
                </Route>
              </Switch>
            </div>
          </Router>
        </QueryClientProvider>
      );
    };
    
    export default App;

    Using useQuery

    src/components/RQSuperHeroes.page.tsx

    import axios from 'axios';
    import { useQuery } from 'react-query';
    import { Hero } from '../typings/Heroes';
    
    const fetchSuperHeroes = () => {
      return axios.get('http://localhost:4000/superheroes');
    };
    
    const RQSuperHeroesPage = () => {
      const { isLoading, data } = useQuery('super-heroes', fetchSuperHeroes);
    
      if (isLoading) {
        return <h2>Loading...</h2>;
      }
    
      return (
        <>
          <h2>RQ Super Heroes Page</h2>
          {data?.data.map((hero: Hero) => (
            <div key={hero.name}>{hero.name}</div>
          ))}
        </>
      );
    };
    
    export default RQSuperHeroesPage;

    Handing Query Error

    Traditional way handling error

    src/components/SuperHeroes.page.tsx

    import { useEffect, useState } from 'react';
    import axios from 'axios';
    import { Hero } from '../typings/Heroes';
    
    const SuperHeroesPage = () => {
      const [isLoading, setIsLoading] = useState(true);
      const [data, setData] = useState<Hero[]>([]);
      const [error, setError] = useState('');
    
      useEffect(() => {
        axios
          .get('http://localhost:4000/superheroes1')
          .then((res) => {
            setData(res.data);
            setIsLoading(false);
          })
          .catch((error) => {
            setError(error.message);
            setIsLoading(false);
          });
      }, []);
    
      if (isLoading) {
        return <h2>Loading...</h2>;
      }
    
      if (error) {
        return <h2>{error}</h2>;
      }
    
      return (
        <>
          <h2>Super Heroes Page</h2>
          {data.map((hero) => {
            return <div key={hero.name}>{hero.name}</div>;
          })}
        </>
      );
    };
    
    export default SuperHeroesPage;

    src/components/RQSuperHeroes.page.tsx

    import axios, { AxiosError, AxiosResponse } from 'axios';
    import { useQuery } from 'react-query';
    import { Hero } from '../typings/Heroes';
    
    const fetchSuperHeroes = () => {
      return axios.get('http://localhost:4000/superheroes');
    };
    
    const RQSuperHeroesPage = () => {
      const { isLoading, data, isError, error } = useQuery<AxiosResponse<Hero[]>, AxiosError>(
        'super-heroes',
        fetchSuperHeroes
      );
    
      if (isLoading) {
        return <h2>Loading...</h2>;
      }
    
      if (isError) {
        return <h2>{error.message}</h2>;
      }
    
      return (
        <>
          <h2>RQ Super Heroes Page</h2>
          {data?.data.map((hero) => (
            <div key={hero.name}>{hero.name}</div>
          ))}
        </>
      );
    };
    
    export default RQSuperHeroesPage;

    React query devtools

    src/App.tsx

    import { BrowserRouter as Router, Link, Route, Switch } from 'react-router-dom';
    import { QueryClientProvider, QueryClient } from 'react-query';
    import { ReactQueryDevtools } from 'react-query/devtools';
    import './App.css';
    import HomePage from './components/Home.page';
    import RQSuperHeroesPage from './components/RQSuperHeroes.page';
    import SuperHeroesPage from './components/SuperHeroes.page';
    
    const queryClient = new QueryClient();
    
    const App = () => {
      return (
        <QueryClientProvider client={queryClient}>
          <Router>
            <div>
              <nav>
                <ul>
                  <li>
                    <Link to="/">Home</Link>
                  </li>
                  <li>
                    <Link to="/super-heroes">Traditional Super Heroes</Link>
                  </li>
                  <li>
                    <Link to="/rq-super-heroes">RQ Super Heroes</Link>
                  </li>
                </ul>
              </nav>
              <Switch>
                <Route path="/super-heroes">
                  <SuperHeroesPage />
                </Route>
                <Route path="/rq-super-heroes">
                  <RQSuperHeroesPage />
                </Route>
                <Route path="/">
                  <HomePage />
                </Route>
              </Switch>
            </div>
          </Router>
          <ReactQueryDevtools initialIsOpen={false} position="bottom-right" />
        </QueryClientProvider>
      );
    };
    
    export default App;

    Query cache

    Query cache that react query provides by default every query result is cached for five minutes and react query relies on that cache for subsequent requests.

    How useQuery works with respect to caching

    const fetchSuperHeroes = () => {
      return axios.get('http://localhost:4000/superheroes');
    };
    
    const RQSuperHeroesPage = () => {
      const { isLoading, data, isError, error } = useQuery<AxiosResponse<Hero[]>, AxiosError>(
        'super-heroes',
        fetchSuperHeroes
      );
    
      // ...
    };
    
    export default RQSuperHeroesPage;
    1. useQuery is fired for super-heroes key.
    2. isLoading is set to true and a network request is sent to fetch the data.
    3. When the request is completed, it is cached using the query key and the fetchSuperHeroes function as the unique idetifiers.
    4. When we navigate to the home page and revisit the /rq-super-heroes page, react query will check if the data for this query exists in cache.
    5. Since it does the cached data is immediately returned without isLoading set to true. That is the reasone we don't see the loading text for subsequent requests.

    However react query knows that the server data might have updated and the cache might not contain the latest data. So a background refetch is triggered for the same query and if the fetch is successful the new data is updated in the UI. Since our data is the same as the cached data, we don't see any change in the UI.

    isFetching

    isLoading is not changed. Does useQuery provide another boolean flag to indicate the background refetching of the query. The answer is yes and the flag is called isFetching.

    // ...
    
    const RQSuperHeroesPage = () => {
      const { isLoading, data, isError, error, isFetching } = useQuery<AxiosResponse<Hero[]>, AxiosError>(
        'super-heroes',
        fetchSuperHeroes
      );
    
      console.log(isLoading, isFetching);
    
      // ...
    };
    
    export default RQSuperHeroesPage;

    If the data has been changed, we will see the cached list from before and then the list updates when the background refetching has completed.
    React query out of the box leads to better user experience as there is a list being displayed already and then the list updates in the background. A user does not have to see the loading indicator every single time.

    Chache time

    React query sets a default value of 5 minutes for the query cache and that is a good default which you can leave as it is. If you really want to change it though pass in a third argument to useQuery.

    // ...
    
    const RQSuperHeroesPage = () => {
      const { isLoading, data, isError, error, isFetching } = useQuery<AxiosResponse<Hero[]>, AxiosError>(
        'super-heroes',
        fetchSuperHeroes,
        { cacheTime: 5000 }
      );
    
      // ...
    };
    
    export default RQSuperHeroesPage;

    Stale time

    Another use of query cache is to reduce the number of network requests for data that doesn't necessarily change too often. For example, let's say our list of superheroes does not change often and it is okay if the user sees stale data for a while. In such cases, we can use the cached query results without having to refetch in the background. To achieve that behavior we configure another property called staleTime. The defualt staleTime is 0 seconds.

    // ...
    
    const RQSuperHeroesPage = () => {
      const { isLoading, data, isError, error, isFetching } = useQuery<AxiosResponse<Hero[]>, AxiosError>(
        'super-heroes',
        fetchSuperHeroes,
        { staleTime: 30000 }
      );
    
      // ...
    };
    
    export default RQSuperHeroesPage;

    Refetch Defaults

    Let's learn two more configurations related to refetching for which react query provides a default value.

    • reetchOnMount
      If it's set to true, the query will refetch on mount if the data is stale. (default)
      If it's set to false, the query will not refetch on mount.
      If it's set to 'always', the query will always refetch the data when the component mount.
    // ...
    
    const RQSuperHeroesPage = () => {
      const { isLoading, data, isError, error, isFetching } = useQuery<AxiosResponse<Hero[]>, AxiosError>(
        'super-heroes',
        fetchSuperHeroes,
        { reetchOnMount: true }
      );
    
      // ...
    };
    
    export default RQSuperHeroesPage;
    • refetchOnWindowFocus
      If a user leaves your application and returns to stale data, React Query automatically requests fresh data for you in the background. You can disable this globally or per-query using the refetchOnWindowFocus option.
      If set to true, the query will refetch on window focus if the data is stale.
      If set to false, the query will not refetch on window focus.
      If set to 'always', the query always refetch on window focust.
    // ...
    
    const RQSuperHeroesPage = () => {
      const { isLoading, data, isError, error, isFetching } = useQuery<AxiosResponse<Hero[]>, AxiosError>(
        'super-heroes',
        fetchSuperHeroes,
        { refetchOnWindowFocus: true }
      );
    
      // ...
    };
    
    export default RQSuperHeroesPage;

    Polling

    Polling basically refers to the process of fetching data at regular intervals. For example, if you have a component that shows the real-time price of different stocks, you might want to fetch data every second to update the UI. This ensures the UI will always be in sync with the remote data irrespective of configurations like refetchOnMount or refetchOnWindowFocus which is dependent on user interaction. Now to pull data with react query, we can make use of another configuration called refetchInterval.

    refetchInterval

    By default, it is set to false. If set to a number, all queries will continuously refetch at this frequency in milliseconds.

    // ...
    
    const RQSuperHeroesPage = () => {
      const { isLoading, data, isError, error, isFetching } = useQuery<AxiosResponse<Hero[]>, AxiosError>(
        'super-heroes',
        fetchSuperHeroes,
        { refetchInterval: 2000 }
      );
    
      // ...
    };
    
    export default RQSuperHeroesPage;

    refetchIntervalInBackground

    The pulling or automatic refetching is paused if the window loses focus. If you do want background refetching at regular intervals, you can specify another configuration call refetchIntervalInBackground and set it to true.

    // ...
    
    const RQSuperHeroesPage = () => {
      const { isLoading, data, isError, error, isFetching } = useQuery<AxiosResponse<Hero[]>, AxiosError>(
        'super-heroes',
        fetchSuperHeroes,
        {
          refetchInterval: 2000,
          refetchIntervalInBackground: true,
        }
      );
    
      // ...
    };
    
    export default RQSuperHeroesPage;

    useQuery on click

    We might have to fetch the data based on a user event and not when the component mounts. There are two steps that we need to implement.

    1. The first step is to inform useQuery not to fire the get request when the component mounts. We do that by passing in a configuration called enabled and setting it to false.
      // ...
      

    const RQSuperHeroesPage = () => {
    const { isLoading, data, isError, error, isFetching } = useQuery<AxiosResponse<Hero[]>, AxiosError>(
    'super-heroes',
    fetchSuperHeroes,
    {
    enabled: false,
    }
    );

    // ...
    };

    export default RQSuperHeroesPage;

    
    2. Step two, we fetch the data on click of a button using `refetch` which is a function returned by `useQuery`.
    ```tsx
    // ...
    
    const RQSuperHeroesPage = () => {
      const { isLoading, data, isError, error, isFetching, refetch } = useQuery<AxiosResponse<Hero[]>, AxiosError>(
        'super-heroes',
        fetchSuperHeroes,
        {
          enabled: false,
        }
      );
    
      // ...
    
      return (
        <>
          <h2>RQ Super Heroes Page</h2>
          <button onClick={() => refetch()}>Fetch heroes</button>
          {data?.data.map((hero) => (
            <div key={hero.name}>{hero.name}</div>
          ))}
        </>
      );
    };
    
    export default RQSuperHeroesPage;

    Success and Error Callbacks

    Sometimes we might want to perform a side effect when the query completes. An example could be opening a modal, navigating to a different route or even displaying toast notifications. To cater to these scenaros react query let us specify auccess and error callbacks as configurations or options to the useQuery hook.

    // ...
    
    const RQSuperHeroesPage = () => {
      const onSuccess = (data: AxiosResponse) => {
        console.log('Perform side effect after data fetching.', data);
      };
    
      const onError = (error: AxiosError) => {
        console.log('Perform side effect after encountering error.', error);
      };
    
      const { isLoading, data, isError, error, isFetching, refetch } = useQuery<AxiosResponse<Hero[]>, AxiosError>(
        'super-heroes',
        fetchSuperHeroes,
        {
          onSuccess,
          onError,
        }
      );
      // ...
    };
    
    export default RQSuperHeroesPage;

    Data Transformation

    You've run into the scenario of needing to transform data into a format that the front-end component can consume. The back-end folks have their own convention and the front-end might have a different convention. To help with such scenarios react query provides a select configuration option which we can specify on the useQuery hook

    // ...
    
    const RQSuperHeroesPage = () => {
      // ...
    
      const { isLoading, data, isError, error, isFetching, refetch } = useQuery<
        AxiosResponse<Hero[]>,
        AxiosError,
        string[]
      >('super-heroes', fetchSuperHeroes, {
        onSuccess,
        onError,
        select: (data) => {
          return data.data.map((hero) => hero.name);
        },
      });
    
      // ...
    
      return (
        <>
          <h2>RQ Super Heroes Page</h2>
          {data?.map((heroName) => (
            <div key={heroName}>{heroName}</div>
          ))}
        </>
      );
    };
    
    export default RQSuperHeroesPage;

    Custom Query Hook

    For larger apps, you might want to reuse the data fetching logic. For example, the same query might be required in a different component. One approach would be to duplicate the code we've written here. However we all know that is not the best way to write code. What we need is a way to reuse the useQuery hook. Let's learn how to create a custom hook that wraps around the useQuery hook. That will allow us to call the custom hook from multiple components without having to duplicate the code.

    src/hooks/useSuperHeroesData.ts

    import axios, { AxiosError, AxiosResponse } from 'axios';
    import { useQuery } from 'react-query';
    import { Hero } from '../typings/Heroes';
    
    const fetchSuperHeroes = () => {
      return axios.get('http://localhost:4000/superheroes');
    };
    
    export const useSuperHeroesData = (
      onSuccess?: (data: string[]) => void,
      onError?: (err: AxiosError<unknown, any>) => void
    ) => {
      return useQuery<AxiosResponse<Hero[]>, AxiosError, string[]>('super-heroes', fetchSuperHeroes, {
        onSuccess,
        onError,
        select: (data) => {
          return data.data.map((hero) => hero.name);
        },
      });
    };

    Query by Id

    Querying by Id setup

    1. Create a new page that will eventually display the details about one single super hero.
    2. Configure the route to that page and add a link from the super heroes list page to the super hero details page.
    3. Fetch a superhero by id and display the details in the UI.

    src/hooks/useSuperHeroData.ts

    import axios from 'axios';
    import { useQuery } from 'react-query';
    
    const fetchSuperHero = ({ queryKey }: { queryKey: string[] }) => {
      const heroId = queryKey[1];
      return axios.get(`http://localhost:4000/superheroes/${heroId}`);
    };
    
    export const useSuperHeroData = (heroId: string) => {
      return useQuery(['super-hero', heroId], fetchSuperHero);
    };

    Parallel Queries

    Sometimes a single component needs to call multiple APIs to fetch the necessary data. With react query, it is as simple as calling useQuery twice.

    import axios from 'axios';
    import { useQuery } from 'react-query';
    
    const fetchSuperHeroes = () => {
      return axios.get('http://localhost:4000/superheroes');
    };
    
    const fetchFriends = () => {
      return axios.get('http://localhost:4000/friends');
    };
    
    const ParallelQuriesPage = () => {
      const { data: superHeroes } = useQuery('super-heroes', fetchSuperHeroes);
      const { data: friends } = useQuery('friends', fetchFriends);
    
      return <div>Parallel Quries Page</div>;
    };
    
    export default ParallelQuriesPage;

    Dynamic Parallel Queries

    If the number of queries you need to execute is changing from render to render, you cannot use manual querying as that would violate the rules of hooks. In other words, simply invoking useQuery multiple times is not sufficient for dynamic parallel queries. To cater to this specific scenario, react query provides another hook called useQueries.

    import axios from 'axios';
    import { useQueries } from 'react-query';
    
    const fetchSuperHero = (heroId: number) => {
      return axios.get(`http://localhost:4000/superheroes/${heroId}`);
    };
    
    const DynamicParallelPage = ({ heroIds }: { heroIds: number[] }) => {
      const queryResults = useQueries(
        heroIds.map((id) => ({
          queryKey: ['super-hero', id],
          queryFn: () => fetchSuperHero(id),
        }))
      );
    
      return <div>Dynamic Parallel Page</div>;
    };
    
    export default DynamicParallelPage;

    Dependent Queries

    You need the queries to execute sequentially. The situation arises when you have one query dependent on the results of another query.

    import axios from 'axios';
    import { useQuery } from 'react-query';
    
    type Props = {
      email: string;
    };
    
    const fetchUserByEmail = (email: string) => {
      return axios.get(`http://localhost:4000/users/${email}`);
    };
    
    const fetchCoursesByChannlId = (channelId: number) => {
      return axios.get(`http://localhost:4000/channels/${channelId}`);
    };
    
    const DepentQueriesPage = ({ email }: Props) => {
      const { data: user } = useQuery(['user', email], () => fetchUserByEmail(email));
    
      const channelId = user?.data.channelId;
    
      const { data: courses } = useQuery(['courses', channelId], () => fetchCoursesByChannlId(channelId), {
        enabled: !!channelId,
      });
    
      return <div>Depent Queries Page</div>;
    };
    
    export default DepentQueriesPage;

    Initial Query Data

    import axios, { AxiosResponse } from 'axios';
    import { useQuery, useQueryClient } from 'react-query';
    import { Hero } from '../typings/Heroes';
    
    const fetchSuperHero = ({ queryKey }: { queryKey: string[] }) => {
      const heroId = queryKey[1];
      return axios.get(`http://localhost:4000/superheroes/${heroId}`);
    };
    
    export const useSuperHeroData = (heroId: string) => {
      const queryClient = useQueryClient();
      return useQuery(['super-hero', heroId], fetchSuperHero, {
        initialData: () => {
          const hero = queryClient
            .getQueryData<AxiosResponse<Hero[]>>('super-heroes')
            ?.data?.find((hero) => hero.id === parseInt(heroId));
    
          if (hero) {
            return {
              data: hero,
            } as AxiosResponse;
          } else {
            return undefined;
          }
        },
      });
    };
    

    Paginated Queries

    If we specify keepPreviousData and set it to true, we add query will maintain the data from the last successful fetch while the new data is being requested.

    import axios from 'axios';
    import { useState } from 'react';
    import { useQuery } from 'react-query';
    
    const fetchColors = (pageNumber: number) => {
      return axios.get(`http://localhost:4000/colors?_limit=2&_page=${pageNumber}`);
    };
    
    const PaginatedQueriesPage = () => {
      const [pageNumber, setPageNumber] = useState(1);
      const { isLoading, isError, error, data } = useQuery(['colors', pageNumber], () => fetchColors(pageNumber), {
        keepPreviousData: true,
      });
    
      if (isLoading) {
        return <h2>Loading...</h2>;
      }
    
      if (isError) {
        return <h2>{error as string}</h2>;
      }
    
      return (
        <div>
          {data?.data.map((color: { id: number; label: string }) => (
            <div key={color.id}>
              <h2>
                {color.id}. {color.label}
              </h2>
            </div>
          ))}
          <div>
            <button onClick={() => setPageNumber(pageNumber - 1)} disabled={pageNumber === 1}>
              Prev page
            </button>
            <button onClick={() => setPageNumber(pageNumber + 1)} disabled={pageNumber === 4}>
              Next page
            </button>
          </div>
        </div>
      );
    };
    
    export default PaginatedQueriesPage;

    Infinite Queries

    import axios from 'axios';
    import { Fragment } from 'react';
    import { useInfiniteQuery } from 'react-query';
    
    const fetchColors = ({ pageParam = 1 }) => {
      return axios.get(`http://localhost:4000/colors?_limit=2&_page=${pageParam}`);
    };
    
    const InfiniteQueries = () => {
      const { isLoading, isError, error, data, hasNextPage, fetchNextPage, isFetching, isFetchingNextPage } =
        useInfiniteQuery(['colors'], fetchColors, {
          getNextPageParam: (_lastPage, pages) => {
            if (pages.length < 4) {
              return pages.length + 1;
            } else {
              return undefined;
            }
          },
        });
    
      if (isLoading) {
        return <h2>Loading...</h2>;
      }
    
      if (isError) {
        return <h2>{error as string}</h2>;
      }
    
      return (
        <>
          <div>
            {data?.pages.map((group, i) => (
              <Fragment key={i}>
                {group.data.map((color: { id: number; label: string }) => (
                  <h2 key={color.id}>
                    {color.id}. {color.label}
                  </h2>
                ))}
              </Fragment>
            ))}
            <div>
              <button disabled={!hasNextPage} onClick={() => fetchNextPage()}>
                Load more
              </button>
            </div>
          </div>
          <h2>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</h2>
        </>
      );
    };
    
    export default InfiniteQueries;

    Mutations

    It's time to focus on the data posting aspect. That is sending data from your application to any backend. In react query unlike queries, mutations are what we use to create update or delete data.
    And this purpose similar to useQuery, the library provides a useMutation hook.

    src/components/RQSuperHeroes.page.tsx

    // ...
    
    const RQSuperHeroesPage = () => {
      const [name, setName] = useState('');
      const [alterEgo, setAlterEgo] = useState('');
    
      const { mutate: addHero, isLoading, isError, error } = useAddSuperHeroData();
    
      const handleAddHeroClick = () => {
        const hero = { name, alterEgo };
        addHero(hero);
      };
    
      if (isLoading) {
        return <h2>Loading...</h2>;
      }
    
      if (isError) {
        return <h2>{error.message}</h2>;
      }
    
      return (
        <>
          <h2>RQ Super Heroes Page</h2>
          <div>
            <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
            <input type="text" value={alterEgo} onChange={(e) => setAlterEgo(e.target.value)} />
            <button onClick={handleAddHeroClick}>Add Hero</button>
          </div>
        </>
      );
    };
    
    export default RQSuperHeroesPage;

    src/hooks/useSuperHeroesData.ts

    const addSuperHero = (hero: { name: string, alterEgo: string }) => {
      return axios.post('http://localhost:4000/superheroes', hero);
    };
    
    export const useAddSuperHeroData = () => {
      return useMutation(addSuperHero);
    };

    Query Invalidation

    Before we have to manually refetch the superhero list by clicking the fetch heroes buton. This is because as soon as we add a new hero. The super heroes query data is out of date. Wouldn't it be nice if we could tell react query to automatically refetch the superheroes query as soon as the mutation succeeds. Well it definitely would be nice and react query makes it really simple to achieve that. The feature is called query invalidation.

    src/hooks/useSuperHeroesData.ts

    const addSuperHero = (hero: { name: string, alterEgo: string }) => {
      return axios.post('http://localhost:4000/superheroes', hero);
    };
    
    export const useAddSuperHeroData = () => {
      const queryClient = useQueryClient();
      return useMutation(addSuperHero, {
        onSuccess: () => {
          queryClient.invalidateQueries('super-heroes');
        },
      });
    };

    Handling Mutation Response

    If you click on the post request, you can see in the response we have the new hero object being returned. It is pretty common for the new object to be automatically returned in the response of the mutation. So instead of refetching a query for this item and wasting a network call for data that we already have, we can take advantage of the object returned by the mutation function and immediately update the existing query with the new data. In similar words, we can use the add superhero mutation response to update the superheroes query data. Thereby saving an additional network request.

    src/hooks/useSuperHeroesData.ts

    const addSuperHero = (hero: { name: string, alterEgo: string }) => {
      return axios.post('http://localhost:4000/superheroes', hero);
    };
    
    export const useAddSuperHeroData = () => {
      const queryClient = useQueryClient();
      return useMutation(addSuperHero, {
        onSuccess: (data) => {
          queryClient.setQueryData('super-heroes', (oldQueryData: any) => {
            return {
              ...oldQueryData,
              data: [...oldQueryData.data, data.data],
            };
          });
        },
      });
    };

    댓글

Designed by Tistory.