# React Query basic

reference: <https://jforj.tistory.com/search/React%20Query>

***

#### useQuery

* useQuery는 **React Query를 이용해 서버로부터 데이터를 조회해올 때 사용**합니다.

  ※ 데이터 조회가 아닌 데이터 변경 작업을 할 때는 \*\*[useMutation](https://jforj.tistory.com/244)\*\*을 사용합니다. 데이터베이스로 비유하자면 select를 할 때 사용

• queryKey: queryKey의 역할은 **React-Query가 query 캐싱을 관리할 수 있도록 도와줌**

• queryFn

```tsx
// 1
const res = useQuery(queryKey, queryFn);

// 2
const res = useQuery({
    queryKey: queryKey,
    queryFn: queryFn
});
```

**option값**

```tsx
// 1
const res = useQuery(['persons'], () => axios.get('<http://localhost:8080/persons>'), {
    staleTime: 5000, // 5초
    cacheTime: Infinity, // 제한 없음
});

// 2
const res = useQuery({
    queryKey: ['persons'],
    queryFn: () => axios.get('<http://localhost:8080/persons>'),
    staleTime: 5000, // 5초
    cacheTime: Infinity // 제한 없음
});
```

**refetchOnWindowFocus 전역설정**

* 방법 1

```tsx
import * as React from 'react';
import ReactDom from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';

const queryClient = new QueryClient(
    {
        defaultOptions: {
            queries: {
                refetchOnWindowFocus: false, // window focus 설정
            }
        }
    }
); // queryClient 생성

ReactDom.render(
    // App에 QueryClient 제공
    <QueryClientProvider client={queryClient}>
        <App />
    </QueryClientProvider>, 
    document.querySelector('#root')
);
```

* 방법 2

```tsx
// 1
const res = useQuery(['persons'], () => axios.get('<http://localhost:8080/persons>'), {
    refetchOnWindowFocus: false // window focus 설정
});

// 2
const res = useQuery({
    queryKey: ['persons'],
    queryFn: () => axios.get('<http://localhost:8080/persons>'),
    refetchOnWindowFocus: false // window focus 설정
});
```

#### query 자동실행 설정

ex)

```tsx
if(id) {
    const res = axios.get('<http://localhost:8080/person>', {
        params: {
            id: id,
        }
    })
}
```

useQuery에서는 if문을 사용하지 않고 useQuery에서 제공해주는 query 자동 실행 설정을 통해 동일한 결과를 만들어 줄 수 있다.

```tsx
// 1
const res = useQuery(['person', id], () => axios.get('<http://localhost:8080/person>', {
    params: {
        id: id,
    }
}), {
    enabled: !!id // 코드 자동 실행 설정
});

// 2
const res1 = useQuery({
    queryKey: ['person', id],
    queryFn: () => axios.get('<http://localhost:8080/person>', {
        params: {
            id: id,
        }
    }),
    enabled: !!id // 코드 자동 실행 설정
});
```

***

***

#### useMutation

useMutation은 **React Query를 이용해 서버에 데이터 변경 작업을 요청할 때 사용**합니다.※ 데이터 조회를 할 때는 useQuery를 사용데이터베이스로 비유하자면 insert, update, delete가 모두 포함

사용형태

```tsx
// 1
const savePerson = useMutation(mutationFn);

// 2
const savePerson = useMutation({
    mutationFn: mutationFn
})
```

**mutationFn**은 `mutation` Function으로 **`promise` 처리가 이루어지는 함수**입니다.

다른 말로는 `axios`를 이용해 서버에 API를 요청하는 부분

```tsx
// 1
const savePerson = useMutation((person: Iperson) => axios.post('/savePerson', person));

// 2
const savePerson = useMutation({
    mutationFn: (person: Iperson) => axios.post('/savePerson', person)
})
```

#### mutate

mutate는 **`useMutation`을 이용해 작성한 내용들이 실행될 수 있도록 도와주는 trigger 역할**

즉, `useMutation`을 정의 해둔 뒤 이벤트가 발생되었을 때 mutate를 사용

```tsx
const savePerson = useMutation((person: Iperson) => axios.post('<http://localhost:8080/savePerson>', person)); // useMutate 정의

    const onSavePerson = () => {
        savePerson.mutate(person); // 데이터 저장
    }

<Person.SaveButton onClick={onSavePerson}>저장</Person.SaveButton>
```

**onSuccess, onError, onSettled**

일반적으로 서버에 데이터 변경 요청을 하게 되면 변경 요청이 성공할 경우에 추가적인 액션을 할 수 있도록 코드를 작성.

이런 상황은 useMutation을 사용할 때도 동일하게 적용.

`async/await`을 사용할 때는 보통 다음과 같이 결괏값이 있는지를 확인한 뒤 추가 작업을 수행할 수 있는 코드를 작성

```tsx
try {
    const res = await axios.post('<http://localhost:8080/savePerson>', person);

    if(res) {
        console.log('success');
    }
} catch(error) {
    console.log('error');
} finally {
    console.log('finally');
}
```

`useMutation` 을 사용하면 다음과 같이 사용

```tsx
// 1
const savePerson = useMutation((person: Iperson) => axios.post('<http://localhost:8080/savePerson>', person), {
    onSuccess: () => { // 요청이 성공한 경우
        console.log('onSuccess');
    },
    onError: (error) => { // 요청에 에러가 발생된 경우
        console.log('onError');
    },
    onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
        console.log('onSettled');
    }
});

// 2
const savePerson = useMutation({
    mutationFn: (person: Iperson) => axios.post('/savePerson', person),
    onSuccess: () => { // 요청이 성공한 경우
        console.log('onSuccess');
    },
    onError: (error) => { // 요청에 에러가 발생된 경우
        console.log('onError');
    },
    onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
        console.log('onSettled');
    }
})
```

`onSettled`는 `finally` 구문처럼 요청이 성공하든 에러가 발생되든 상관없이 마지막에 실행되는 구간

`onSuccess, onError, onSettled`는 `useMutation`을 정의할 때만 사용할 수 있는 것이 아니라 `mutate`에서도 사용이 가능합니다.

```tsx
const onSavePerson = () => {
    savePerson.mutate(person, {
        onSuccess: () => { // 요청이 성공한 경우
            console.log('onSuccess');
        },
        onError: (error) => { // 요청에 에러가 발생된 경우
            console.log('onError');
        },
        onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
            console.log('onSettled');
        }
    }); // 데이터 저장
}
```

#### invalidateQueries

* `useQuery`에서 사용되는 `queryKey`의 유효성을 제거해주는 목적으로 사용
* `queryKey`의 유효성을 제거해주는 이유는 **서버로부터 다시 데이터를 조회해오기 위함**
* 정해진 시간이 도달하지 않으면 새로운 데이터가 적재되었더라도 `useQuery`는 변동 없이 동일한 데이터를 화면에 보여줄 것입니다.
* 결국 사용자 입장에서는 데이터 생성이 제대로 되었는지에 대한 파악이 힘들기 때문에 혼란을 겪을 수 있게 됩니다.
* 해당 상황을 해결해줄 수 있는 것이 바로 `invalidateQueries`입니다.
* 데이터를 저장할 때 `invalidateQueries`를 이용해 `useQuery`가 가지고 있던 `queryKey`의 유효성을 제거해주면 캐싱되어있는 데이터를 화면에 보여주지 않고 서버에 새롭게 데이터를 요청하게 됩니다.
* 결국 데이터가 새롭게 추가되었을 때 다시 서버에서 데이터를 가져오게 되면서 추가한 데이터까지 화면에서 확인할 수 있게 됩니다.

```tsx
const queryClient = useQueryClient();  // 등록된 quieryClient 가져오기

const savePerson = useMutation((person: Iperson) => axios.post('<http://localhost:8080/savePerson>', person), {
    onSuccess: () => { // 요청이 성공한 경우
        console.log('onSuccess');
        queryClient.invalidateQueries('persons'); // queryKey 유효성 제거
    },
    onError: (error) => { // 요청에 에러가 발생된 경우
        console.log('onError');
    },
    onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
        console.log('onSettled');
    }
}); // useMutate 정의
```

***

#### setQueryData

* `invalidateQueries`를 사용하지 않고도 데이터를 업데이트해줄 수 있는 방법은 있습니다.

  `setQueryData`를 활용하면 되는데 `setQueryData`는 **기존에 `queryKey`에 매핑되어 있는 데이터를 새롭게 정의**해줍니다.

```tsx
const queryClient = useQueryClient();  // 등록된 quieryClient 가져오기

const savePerson = useMutation((person: Iperson) => axios.post('<http://localhost:8080/savePerson>', person), {
    onSuccess: () => { // 요청이 성공한 경우
        console.log('onSuccess');
        queryClient.setQueryData('persons', (data) => {
            const curPersons = data as AxiosResponse<any, any>; // persons의 현재 데이터 확인
            curPersons.data.push(person); // 데이터 push

            return curPersons; // 변경된 데이터로 set
        })
    },
    onError: (error) => { // 요청에 에러가 발생된 경우
        console.log('onError');
    },
    onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
        console.log('onSettled');
    }
}); // useMutate 정의
```

***

#### useQueries

`useQueries`는 **React Query에서 `useQuery`의 동적 병렬 쿼리 작업을 위해 사용**

`useQueries`의 사용 방법은 단순하게 `useQuery`를 배열로 넣어준다

```tsx
const ress = useQueries([
    useQuery1,
    useQuery2,
    useQuery3,
    ...
]);
```

```tsx
const res1 = useQuery(['persons'], () => axios.get('<http://localhost:8080/persons>'), {
});
const res2 = useQuery(['person'], () => axios.get('<http://localhost:8080/person>', {
    params: {
        id: 1
    }
}));
```

```tsx
const res = useQueries([
    {
        queryKey: ['persons'],
        queryFn: () => axios.get('<http://localhost:8080/persons>'),
    },
    {
        queryKey: ['person'],
        queryFn: () => axios.get('<http://localhost:8080/person>', {
            params: {
                id: 1
            }
        }),
    }
]);
```

**`useQuery`보다 `useQueries`를 사용해야 하는 상황은 동적으로 변화하는 상황**

```tsx
const getPersons = (persons: Iperson[]) => {
  const res = useQueries(
    persons.map((person) => {
      return {
        queryKey: ['person', person.id],
        queryFn: () =>
          axios.get('<http://localhost:8080/person>', {
            params: {
              id: person.id,
            },
          }),
      };
    })
  );

  return (
    <Person.Container>
      {ress &&
        ress.map((res) => {
          const person: Iperson = res.data && res.data.data;
          return (
            person && (
              <Person.Box key={person.id}>
                <Person.Title>{person.id}.</Person.Title>
                <Person.Text>{person.name}</Person.Text>
                <Person.Text>({person.age})</Person.Text>
              </Person.Box>
            )
          );
        })}
    </Person.Container>
  );
};
```

**Suspense**

React Query의 `suspense` 모드를 설정하게 되면 `useQuery`의 status, error 등을 `React.Suspense`로 대체

```tsx
// App.tsx

import * as React from 'react';
import Queries from './pages/queries';

const App = (): JSX.Element => {
  return <Queries />;
};

export default App;
```

```tsx
// queries.tsx

import axios from 'axios';
import * as React from 'react';
import { useQuery } from 'react-query';
import styled from 'styled-components';

interface Iperson {
  id: number;
  name: string;
  phone: string;
  age: number;
}

const Queries = (): JSX.Element => {
  const getPersons = () => {
    const res = useQuery(['persons'], () =>
      axios.get('<http://localhost:8080/persons>')
    );

    // 로딩 중일 경우
    if (res.isLoading) {
      return <LoadingText>Queries Loading...</LoadingText>;
    }

    // 결과값이 전달되었을 경우
    const persons: Iperson[] = res.data && res.data.data;

    return (
      <Person.Container>
        {persons &&
          persons.map((person) => {
            return (
              <Person.Box key={person.id}>
                <Person.Title>{person.id}.</Person.Title>
                <Person.Text>{person.name}</Person.Text>
                <Person.Text>({person.age})</Person.Text>
              </Person.Box>
            );
          })}
      </Person.Container>
    );
  };

  return <Wrapper>{getPersons()}</Wrapper>;
};

export default Queries;

// styled
const Wrapper = styled.div`
  max-width: 728px;

  margin: 0 auto;
`;

const LoadingText = styled.h3`
  text-align: center;
`;

const Person = {
  Container: styled.div`
    padding: 8px;
  `,

  Box: styled.div`
    border-bottom: 2px solid olive;
  `,

  Title: styled.h2`
    display: inline-block;

    margin: 0 12px;

    line-height: 48px;
  `,

  Text: styled.span`
    margin: 0 6px;
  `,
};
```

`React.Suspense`를 이용해 다음과 같이 변경

첫 번째는 App.tsx의 Queries Component가 React.Suspense로 덮여있다는 것

두 번째는 queries.tsx의 useQuery에 suspense 모드 설정이 추가

세 번째는 queries.tsx에서 로딩 중일 경우에 대한 코드가 삭제

```tsx
// App.tsx

import * as React from 'react';
import Queries from './pages/queries';

const App = (): JSX.Element => {
  return (
    <React.Suspense fallback={<div>App Loading...</div>}>
      <Queries />
    </React.Suspense>
  );
};

export default App;
```

```tsx
// queries.tsx

import axios from 'axios';
import * as React from 'react';
import { useQuery } from 'react-query';
import styled from 'styled-components';

interface Iperson {
  id: number;
  name: string;
  phone: string;
  age: number;
}

const Queries = (): JSX.Element => {
  const getPersons = () => {
    const res = useQuery(
      ['persons'],
      () => axios.get('<http://localhost:8080/persons>'),
      {
        suspense: true, // suspense 모드 설정
      }
    );

    // 결과값이 전달되었을 경우
    const persons: Iperson[] = res.data && res.data.data;

    return (
      <Person.Container>
        {persons &&
          persons.map((person) => {
            return (
              <Person.Box key={person.id}>
                <Person.Title>{person.id}.</Person.Title>
                <Person.Text>{person.name}</Person.Text>
                <Person.Text>({person.age})</Person.Text>
              </Person.Box>
            );
          })}
      </Person.Container>
    );
  };

  return <Wrapper>{getPersons()}</Wrapper>;
};

export default Queries;

const Wrapper = styled.div`
  max-width: 728px;

  margin: 0 auto;
`;

const Person = {
  Container: styled.div`
    padding: 8px;
  `,

  Box: styled.div`
    border-bottom: 2px solid olive;
  `,

  Title: styled.h2`
    display: inline-block;

    margin: 0 12px;

    line-height: 48px;
  `,

  Text: styled.span`
    margin: 0 6px;
  `,
};
```

**`suspense` 모드에서 `useQueries`를 사용**

`suspense` 모드에서 useQueries를 사용해야 하는 이유는 useQuery를 병렬로 처리하여 사용하고 있을 때 만약 **하나의 useQuery가 정상적으로 동작되지 않을 경우 그 이후에 실행될 useQuery에 영향** 을 미치며 결과적으로 올바른 화면이 보이지 않기 때문

첫 번째 요소는 걸러지고 두 번째 요소에 해당하는 결과만 화면에 노출

```tsx
import axios from 'axios';
import * as React from 'react';
import { useQueries } from 'react-query';
import styled from 'styled-components';

interface Iperson {
  id: number;
  name: string;
  phone: string;
  age: number;
}

const Queries = (): JSX.Element => {
  const getPersons = () => {
    const res = useQueries([
      {
        queryKey: ['persons'],
        queryFn: () => axios.get('<http://localhost:8080/personssss>'), // 오류
        suspense: true,
      },
      {
        queryKey: ['person'],
        queryFn: () =>
          axios.get('<http://localhost:8080/person>', {
            params: {
              id: 1,
            },
          }),
        suspense: true,
      },
    ]);

    if (res) {
      const persons: Iperson[] = res[0].data && res[0].data.data;
      const person: Iperson = res[1].data && res[1].data.data;

      return (
        <Person.Container>
                  {persons && persons.map((person) => {
              return (
                <Person.Box key={person.id}>
                  <Person.Title>{person.id}.</Person.Title>
                  <Person.Text>{person.name}</Person.Text>
                  <Person.Text>({person.age})</Person.Text>
                </Person.Box>
              );
            })}

                  {person && (
                    <Person.Box>
                        <Person.Title>{person.id}.</Person.Title>
                        <Person.Text>{person.name}</Person.Text>
                        <Person.Text>({person.age})</Person.Text>
                    </Person.Box>
                  )}
          )}
        </Person.Container>
      );
    }
  };

  return <Wrapper>{getPersons()}</Wrapper>;
};

export default Queries;

const Wrapper = styled.div`
  max-width: 728px;

  margin: 0 auto;
`;

const Person = {
  Container: styled.div`
    padding: 8px;
  `,

  Box: styled.div`
    border-bottom: 2px solid olive;
  `,

  Title: styled.h2`
    display: inline-block;

    margin: 0 12px;

    line-height: 48px;
  `,

  Text: styled.span`
    margin: 0 6px;
  `,
};
```

***

#### useInfiniteQuery

`useInfiniteQuery`는 **파라미터 값만 변경하여 동일한 `useQuery`를 무한정 호출할 때 사용**

`const res = useInfiniteQuery(queryKey, queryFn);`

* **pageParam**

`pageParam`은 **`useInfiniteQuery`가 현재 어떤 페이지에 있는지를 확인할 수 있는 파라미터 값**

```tsx
const res = useInfiniteQuery(['infinitePerson'], ({ pageParam = 5 }) =>
  axios.get('<http://localhost:8080/person>', {
    params: {
      id: pageParam,
    },
  })
);
```

기본 값은 `undefined`이기 때문에 값이 없을 경우 초기값으로 5 설정

데이터를 조회해올 때 `pageParam`값을 api 요청할 때 파라미터 값으로 넣어 사용

* **getNextPageParam과 fetchNextPage**

`getNextPageParam`과 `fetchNextPage`은 공통적으로 **다음 페이지에 있는 데이터를 조회해올 때 사용**

* getNextPageParam은 **다음 api를 요청할 때 사용될 pageParam값을 정할 수 있다**

```tsx
const res = useInfiniteQuery(
  ['infinitePerson'],
  ({ pageParam = 5 }) =>
    axios.get('<http://localhost:8080/person>', {
      params: {
        id: pageParam,
      },
    }),
  {
    getNextPageParam: (lastPage, allPages) => {
      return lastPage.data.id + 1; // 다음 페이지를 호출할 때 사용 될 pageParam
    },
  }
);
```

`lastPage`는 `useInfiniteQuery`를 이용해 호출된 가장 마지막에 있는 페이지 데이터를 의미합니다.

`allPages`는 `useInfiniteQuery`를 이용해 호출된 모든 페이지 데이터를 의미

**`return` 되는 값이 다음 페이지가 호출될 때 `pageParam` 값**

* fetchNextPage는 **다음 페이지의 데이터를 호출할 때 사용**

useInfiniteQuery의 return 값에 포함되며 다음과 같이 버튼을 클릭할 때 실행될 이벤트로 등록해줄 수 있다

```tsx
const getPersons = () => {
  const res = useInfiniteQuery(
    ['infinitePerson'],
    ({ pageParam = 5 }) =>
      axios.get('<http://localhost:8080/person>', {
        params: {
          id: pageParam,
        },
      }),
    {
      getNextPageParam: (lastPage, allPages) => {
        return lastPage.data.id + 1; // 다음 페이지를 호출할 때 사용 될 pageParam
      },
    }
  );

  // 로딩 중일 경우
  if (res.isLoading) {
    return <LoadingText>Loading...</LoadingText>;
  }

  // 결과값이 전달되었을 경우
  if (res.data) {
    return (
      <Person.Container>
        {res.data.pages.map((page) => {
          const person: Iperson = page.data;

          return (
            <Person.Box key={person.id}>
              <Person.Title>{person.id}.</Person.Title>
              <Person.Text>{person.name}</Person.Text>
              <Person.Text>({person.age})</Person.Text>
            </Person.Box>
          );
        })}
        <Person.NextButton onClick={() => res.fetchNextPage()}>
          Next
        </Person.NextButton>{' '}
        {/* 클릭 시 다음 페이지 호출 */}
      </Person.Container>
    );
  }
};
```

#### Infinite Scroll

```tsx
import axios from 'axios';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';

// info interface
interface Info {
  name: string;
  phone: string;
  age: number;
}

const App = (): JSX.Element => {
  // state
  const [infoArray, setInfoArray] = useState<Info[]>([]);

  // ref
  const observerRef = useRef<IntersectionObserver>();
  const boxRef = useRef<HTMLDivElement>(null);

  // useEffect
  useEffect(() => {
    getInfo();
  }, []);

  useEffect(() => {
    observerRef.current = new IntersectionObserver(intersectionObserver); // IntersectionObserver
    boxRef.current && observerRef.current.observe(boxRef.current);
  }, [infoArray]);

  // function
  const getInfo = async () => {
    const res = await axios.get('<http://localhost:8080/rest/getInfo>'); // 서버에서 데이터 가져오기
    setInfoArray((curInfoArray) => [...curInfoArray, ...res.data]); // state에 추가

    // console.log('info data add...');
  };

  // IntersectionObserver 설정
  const intersectionObserver = (
    entries: IntersectionObserverEntry[],
    io: IntersectionObserver
  ) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        // 관찰하고 있는 entry가 화면에 보여지는 경우
        io.unobserve(entry.target); // entry 관찰 해제
        getInfo(); // 데이터 가져오기
      }
    });
  };

  // style
  const Wrapper = {
    width: '800px',
    margin: '0 auto',
  };

  const Box = {
    border: '1px solid olive',
    borderRadius: '8px',
    boxShadow: '1px 1px 2px olive',
    margin: '18px 0',
  };

  const BoxTable = {
    borderSpacing: '15px',
  };

  const Title = {
    fontWeight: 700,
  };

  return (
    <div style={Wrapper}>
      {infoArray.map((info, index) => {
        if (infoArray.length - 5 === index) {
          // 관찰되는 요소가 있는 html, 아래에서 5번째에 해당하는 박스를 관찰
          return (
            <div style={Box} ref={boxRef} key={index}>
              <table style={BoxTable}>
                <tbody>
                  <tr>
                    <td style={Title}>이름</td>
                    <td>{info.name}</td>
                  </tr>

                  <tr>
                    <td style={Title}>전화번호</td>
                    <td>{info.phone}</td>
                  </tr>

                  <tr>
                    <td style={Title}>나이</td>
                    <td>{info.age}</td>
                  </tr>
                </tbody>
              </table>
            </div>
          );
        }
        // 관찰되는 요소가 없는 html
        return (
          <div style={Box} key={index}>
            <table style={BoxTable} key={index}>
              <tbody>
                <tr>
                  <td style={Title}>이름</td>
                  <td>{info.name}</td>
                </tr>

                <tr>
                  <td style={Title}>전화번호</td>
                  <td>{info.phone}</td>
                </tr>

                <tr>
                  <td style={Title}>나이</td>
                  <td>{info.age}</td>
                </tr>
              </tbody>
            </table>
          </div>
        );
      })}
    </div>
  );
};

export default App;
```
