React-Query 튜토리얼(3)

오호·2022년 2월 25일
6
post-custom-banner

react query

지금까지는 componentDidMount시점에 패칭하는 것을 해봤는데 이벤트에 따라서 패칭하는 방법을 알아보자.

1. refetch with button

 const { isLoading, data, isError, error, isFetching } = useQuery(
    'super-heroes',
    fetchSuperHeroes,
    {
      enabled: false,
    },
 );

먼저 enabled값을 false로 주자.

그 다음 버튼 엘리먼트를 추가해주고 useQuery의 refetch를 onclick의 값으로 전달해주면 된다.

 const { refetch } = useQuery(
    'super-heroes',
    fetchSuperHeroes,
    {
      enabled: false,
    },
  );

return (
    <>
      <h2>RQ Super Heroes Page</h2>
      <button onClick={refetch}>Fetch heroes</button>
      {data?.data.map((hero) => {
        return <div key={hero.name}>{hero.name}</div>;
      })}
    </>
  );

2. Success and Error Callback

 const onSuccess = () => {
    console.log('perform side effect after data fetching');
  };

  const onError = () => {
    console.log('perform side effect after encountering error');
  };

  const { isLoading, data, isError, error, isFetching, refetch } = useQuery(
    'super-heroes',
    fetchSuperHeroes,
    {
      onSuccess,
      onError,
    },
  );

다음과 같이 데이터패칭에 성공했을 때와 에러가 났을 때를 구별해서 콜백을 전달해줄 수 있다.

3. Data Transformation

 const { isLoading, data, isError, error, isFetching, refetch } = useQuery(
    'super-heroes',
    fetchSuperHeroes,
    {
      onSuccess,
      onError,
      select: (data) => {
        const superHeroNames = data.data.map((hero) => hero.name);
        return superHeroNames;
      },
    },
  );

받아온 데이터를 바로 select라는 프로퍼티 안에서 바꿔줄 수 있다.

4. Custom Query Hook

React-Query를 커스텀훅으로 구현해보자.

hooks/useSuperHeroesData.js를 생성해보자.

import axios from 'axios';
import { useQuery } from 'react-query';

const fetchSuperHeroes = () => {
  return axios.get('http://localhost:4000/superheroes');
};

export function useSuperHeroesData(onSuccess, onError) {
  return useQuery('super-heroes', fetchSuperHeroes, {
    onSuccess,
    onError,
    select: (data) => {
      const superHeroNames = data.data.map((hero) => hero.name);
      return superHeroNames;
    },
  });
}

RQSuperHeroes.page.js

import React from 'react';
import { useSuperHeroesData } from '../hooks/useSuperHeroesData';

function RQSuperHeroes() {
  const onSuccess = () => {
    console.log('perform side effect after data fetching');
  };

  const onError = () => {
    console.log('perform side effect after encountering error');
  };

  const { isLoading, data, isError, error, isFetching, refetch } =
    useSuperHeroesData(onSuccess, onError);

  if (isLoading || isFetching) {
    return <h2>Loading....</h2>;
  }

  if (isError) {
    return <h2>{error.message}</h2>;
  }

  console.log({ isLoading, isFetching });

  return (
    <>
      <h2>RQ Super Heroes Page</h2>
      <button onClick={refetch}>Fetch heroes</button>
      {/* {data?.data.map((hero) => {
        return <div key={hero.name}>{hero.name}</div>;
      })} */}
      {data.map((heroName) => (
        <div>{heroName}</div>
      ))}
    </>
  );
}
export default RQSuperHeroes;

5. Query by Id

다이나믹 라우팅을 설정해보자.

 <Route path="/rq-super-heroes/:heroId" element={<RQSuperHeroPage />} />

App.js에 위 코드를 추가한다.

return (
    <>
      <h2>RQ Super Heroes Page</h2>
      <button onClick={refetch}>Fetch heroes</button>
      {data?.data.map((hero) => {
        return (
          <div key={hero.id}>
            <Link to={`/rq-super-heroes/${hero.id}`}>{hero.name}</Link>
          </div>
        );
      })}
    </>

Link 컴포넌트를 추가하여 이동할 수 있도록 해준다.

새로운 커스텀 훅을 추가해보자!

import axios from 'axios';
import { useQuery } from 'react-query';

const fetchSuperHero = (heroId) => {
  return axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

export function useSuperHeroesData(heroId) {
  return useQuery(['super-hero', heroId], () => fetchSuperHero(heroId));
}

마지막으로 컴포넌트에서 불러와 렌더링해주면된다.

import React from 'react';
import { useParams } from 'react-router-dom';
import { useSuperHeroData } from '../hooks/useSuperHeroData';

function RQSuperHeroPage() {
  const { heroId } = useParams();
  const { isLoading, data, isError, error } = useSuperHeroData(heroId);

  if (isLoading) {
    return <h2>Loading...</h2>;
  }

  if (isError) {
    return <h2>{error.messsage}</h2>;
  }
  return (
    <div>
      {data?.data.name} - {data?.data.alterEgo}
    </div>
  );
}

export default RQSuperHeroPage;

6. parallel Queries

우리는 하나의 컴포넌트에서 여러 개의 쿼리를 사용해야 할 때도 많다. 예제를 위해 db.json에 데이터를 추가해보자.

{
  "superheroes": [
    { "id": 1, "name": "Batman", "alterEgo": "Bruce Wayne" },
    { "id": 2, "name": "Spiderman", "alterEgo": "Peter Parker" },
    { "id": 3, "name": "Ironman", "alterEgo": "Tony Stark" }
  ],
  "friends": [
    { "id": 1, "name": "Chandler Bing" },
    { "id": 2, "name": "Joey Tribbiani" },
    { "id": 3, "name": "Rachel Green" }
  ]
}

ParallelQueries.Page.js를 추가해보자.

import React from 'react';

function ParallelQueriesPage() {
  return <div>ParallelQueries.page</div>;
}

export default ParallelQueriesPage;

물론 라우팅도 추가해준다. (생략)

import axios from 'axios';
import React from 'react';
import { useQuery } from 'react-query';

const fetchSuperHeroes = () => {
  return axios.get('http://localhost:4000/superheroes');
};

const fetchFriends = () => {
  return axios.get('http://localhost:4000/friends');
};

function ParallelQueriesPage() {
  const { data: superHeroes } = useQuery('super-heroes', fetchSuperHeroes);
  const { data: friends } = useQuery('friends', fetchFriends);

  return <div>ParallelQueries.page</div>;
}

export default ParallelQueriesPage;

컴포넌트에서는 이렇게 디스트럭쳐링해서 사용할 수 있다.

7. Dynamic Parallel Queries

DynamicParallelPage

import axios from 'axios';
import React from 'react';
import { useQuery } from 'react-query';

const fetchSuperHero = (heroId) => {
  return axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

function DynamicParallelPage({heroIds}) {
  return <div>DynamicParallelPage</div>;
}

export default DynamicParallelPage;

하나의 히어로가 아니라 여러 히어로를 패치받을것이기 때문에 배열로 받을 것이다.

<Route path="/rq-dynamic-parallel" element={<DynamicParallelPage heroIds={[1, 3]} />} />

다음 라우팅을 추가해주고 히어로Id를 넘겨준다.
이번 예제에서는 몇 개의 React-Query를 사용할지 예정되어있는게 아니므로 이전 6번처럼 수동으로 두 번 호출하는 것으로 해결할 수 없다.

useQueries 를 사용해야한다.

function DynamicParallelPage({ heroIds }) {
  const queryResults = useQueries(
    heroIds.map((id) => {
      return {
        queryKey: ['super-hero', id],
        queryFn: () => fetchSuperHero(id),
      };
    }),
  );
  return <div>DynamicParallelPage</div>;
}

이렇게 사용해서 쿼리를 두 개로 분리해보자.
useQueries의 리턴으로 배열이 오는 것을 확인할 수 있다.

8. Dependent Queries

import React from 'react';

function DependentQueriesPage() {
  return <div>DependentQueriesPage</div>;
}

export default DependentQueriesPage;
 <Route path="/rq-dependent" element={<DependentQueriesPage />} email="leo@example.com" />

예제를 위해서 이번에도 컴포넌트와 라우팅을 추가해주자!

db.json에도 컬럼을 추가해주자.

{
  "superheroes": [
    { "id": 1, "name": "Batman", "alterEgo": "Bruce Wayne" },
    { "id": 2, "name": "Spiderman", "alterEgo": "Peter Parker" },
    { "id": 3, "name": "Ironman", "alterEgo": "Tony Stark" }
  ],
  "friends": [
    { "id": 1, "name": "Chandler Bing" },
    { "id": 2, "name": "Joey Tribbiani" },
    { "id": 3, "name": "Rachel Green" }
  ],
  "users": [
    {
      "id": "leo@example.com",
      "channelId": "messi"
    }
  ],
  "channels": [{ "id": "codevolution", "courses": ["react,vue,angular"] }]
}

일단 users의 email로 데이터를 받아올 것이다.

import axios from 'axios';
import React from 'react';
import { useQuery } from 'react-query';

const fetchUserByEmail = (email) => {
  return axios.get(`http://localhost:4000/users/${email}`);
};

function DependentQueriesPage(email) {
  const { data: user } = useQuery(['user', email], () =>
    fetchUserByEmail(email),
  );

  const channelId = user?.data.channelId;

  return <div>DependentQueriesPage</div>;
}

export default DependentQueriesPage;

그리고 가져온 channelId를 바탕으로 다시 한 번 패칭을 해준다.

import axios from 'axios';
import React from 'react';
import { useQuery } from 'react-query';

const fetchUserByEmail = (email) => {
  return axios.get(`http://localhost:4000/users/${email}`);
};

const fetchCoursesByChannelId = (channelId) => {
  return axios.get(`http://localhost:4000/channels/${channelId}`);
};

function DependentQueriesPage(email) {
  const { data: user } = useQuery(['user', email], () =>
    fetchUserByEmail(email),
  );

  const channelId = user?.data.channelId;

  useQuery(['courses', channelId], () => fetchCoursesByChannelId(channelId), {
    enabled: !!channelId,
  });

  return <div>DependentQueriesPage</div>;
}

export default DependentQueriesPage;

여기서 중요한 점은 channelId가 있을 때만 두 번째 쿼리를 실행할 수 있다. 따라서 enabled처리를 해주자.

9. Initial Query Data

지금까지는 /rq-super-heroes에 들어가서 params로 넘겨준 후에 params를 컴포넌트에서 받아서 이를 통해 hero detail에 대한 정보를 패치해서 받아왔다.

문제는 이전 컴포넌트에서 superheroes에 대한 정보를 모두 가지고 있음에도 다시한번 패칭을 하기 때문에 로딩이 두 번 렌더링되는 UI 측면에서 좋지 않은 현상을 겪었다.

이를 해결해보자.

useSuperHerodata.js

import axios from 'axios';
import { useQuery, useQueryClient } from 'react-query';

const fetchSuperHero = (heroId) => {
  return axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

export function useSuperHeroData(heroId) {
  const queryClient = useQueryClient();

  return useQuery(['super-hero', heroId], () => fetchSuperHero(heroId), {
    initialData: () => {
      const hero = queryClient
        .getQueryData('super-heroes')
        ?.data?.find((hero) => hero.id === parseInt(heroId));

      if (hero) {
        return {
          data: hero,
        };
      } else {
        return undefined;
      }
    },
  });
}

useQueryClient훅은 이전에 사용된 데이터에 대한 캐시를 이용할 수 있다. super-heroes의 캐시 데이터를 이용해서 id를 통해 데이터를 찾아서 리턴하고 렌더링시켜준다.

로딩 인디케이터가 안나타나는 것을 확인할 수 있다.

10. Mutations

지금까지는 모두 Get에 관련된 기능들을 살펴보았다. CRUD에서 R에 해당하는 것만 보았고 이젠 나머지 CUD의 상황에 사용해야할 것들을 살펴보자.

RQSuperHeroesPage.js

import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { useSuperHeroesData } from '../hooks/useSuperHeroesData';

function RQSuperHeroes() {
  const [name, setName] = useState('');
  const [alterEgo, setAlterEgo] = useState('');

  const onSuccess = () => {
    console.log('perform side effect after data fetching');
  };

  const onError = () => {
    console.log('perform side effect after encountering error');
  };

  const { isLoading, data, isError, error, isFetching, refetch } =
    useSuperHeroesData(onSuccess, onError);
    
  const handleAddHeroClick = () => {
    console.log({ name, alterEgo });
  };

  if (isLoading || isFetching) {
    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>
      <button onClick={refetch}>Fetch heroes</button>
      {data?.data.map((hero) => {
        return (
          <div key={hero.id}>
            <Link to={`/rq-super-heroes/${hero.id}`}>{hero.name}</Link>
          </div>
        );
      })}
    </>
  );
}
export default RQSuperHeroes;

input 두개를 추가하고 state를 바인딩해주었다.

useSuperHeroesData.js에 다음과 같이 추가해주자.

import axios from 'axios';
import { useQuery, useMutation } from 'react-query';

const fetchSuperHeroes = () => {
  return axios.get('http://localhost:4000/superheroes');
};

const addSuperHero = (hero) => {
  return axios.post('http://localhost:4000/superheroes', hero);
};

export function useSuperHeroesData(onSuccess, onError) {
  return useQuery('super-heroes', fetchSuperHeroes, {
    onSuccess,
    onError,
  });
}

export const useAddSuperHeroData = () => {
  return useMutation(addSuperHero);
};

마지막으로 컴포넌트 레벨에서 다음을 추가하자.

  const { mutate } = useAddSuperHeroData();

  const handleAddHeroClick = () => {
    console.log({ name, alterEgo });
    const hero = { name, alterEgo };
    mutate(hero);
  };

10. Query Invalidation

mutation이 되었지만 즉각적으로 반응해서 리패치를 진행하지는 않았다. 이를 간단하게 해결해보자.

export const useAddSuperHeroData = () => {
  const queryClient = useQueryClient();

  return useMutation(addSuperHero, {
    onSuccess: () => {
      queryClient.invalidateQueries('super-heroes');
    },
  });
};

useMutation의 두 번째 인자로 성공 콜백을 넘겨주면서 query키를 등록해주면 잘 동작한다.

profile
오호
post-custom-banner

0개의 댓글