React Query - Простое управление серверным стейтом в React
Tags: react, webdev, translate, performance
Published: 2020-05-21 17:29:28

Вольный перевод статьи [Getting started with React Query - Easy server state management in React](https://dev.to/vincenius/getting-started-with-react-query-easy-server-state-management-in-react-536l)

Обработка стейта, исходящего от сервера, может действительно вызвать некоторые проблемы в React. Есть много вещей, о которых вы должны подумать при работе с асинхронными данными, таких как обновление, кэширование или повторная загрузка.

Вот тут-то и пригождается [react-query](https://www.npmjs.com/package/react-query). Он легко справляется с ними, а также предлагает простые решения для оптимистичного рендеринга, бесконечного скроллинга, пагинации и многого другого.

Вот небольшое демо того, что мы будем строить:

![](https://dev-to-uploads.s3.amazonaws.com/i/sf3cwalbundyin2ej6di.gif)

Если вы хотите перейти прямо в код, вы можете найти репозиторий здесь: https://github.com/wwebdev/react-query-demo

В этом уроке я предполагаю, что у вас установлена `node`. Прежде всего, создадим новое react приложение с помощью `npx create-react-app`. После этого установим `react-query` с помощью `npm i --save react-query`.

Для демонстрации того, как работает react-query, я буду использовать [Json Placeholder API](https://jsonplaceholder.typicode.com/) для создания простого блога.

# Загрузка данных

Прежде всего, удалим весь шаблонный код в App.js и заменим его следующим кодом:

```jsx
import React from 'react';
import { useQuery } from 'react-query'

const getPosts = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts')
  return response.json()
}

function App() {
  const { status, data, isFetching, error } = useQuery('posts', getPosts)

  if (status === 'loading') {
    return <div>loading...</div> // loading state
  }

  if (status === 'error') {
    return <div>{error.message}</div> // error state
  }

  return (
    <div>
      { data && <ul>{
        data
          .slice(0,10) // only take frist 10 for now
          // render list of titles
          .map(d => <li key={`post-${d.id}`}>{d.title}</li>)
      }</ul> }
      { isFetching && <p>updating...</p> }
    </div>
  )
}

export default App
```

Сначала определили функцию getPosts - она может включать в себя все, что угодно, если она возвращает асинхронную функцию.

В начале App() вызывается хук useQuery с идентификатором для данных, которые извлекаются, и с асинхронной функции getPosts.

Хук возвращает status, data, isFetching и error. Это довольно хорошо описывает себя. Статус может быть «success», «loading» или «error». Остальная часть компонента отображает результат в трех возможных состояниях.

Внутренние компоненты react-query теперь позаботятся обо всей логике кэширования и обновления. Это означает, что всякий раз, когда вы переходите на страницу, вы будете знать, что отображаемые данные будут там мгновенно, если вы предварительно извлекли их, и они всегда будут в актуальном серверном состоянии.

Это в принципе все, что вам нужно знать, чтобы начать использовать react-query. Но давайте расширим этот пример, чтобы увидеть кэширование и обновление в действии!

## Расширение приложения

Прежде всего, я перенесу код из App.js в новый компонент components/Home.js. Далее я переименую компонент и добавлю NavLink в список сообщений.

```jsx
import React from 'react'
import { NavLink } from 'react-router-dom'
import { useQuery } from 'react-query'

const getPosts = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts')
  await new Promise(r => setTimeout(r, 1000)) // wait a second
  return response.json()
};

function Home() {
  const { status, data, isFetching, error } = useQuery('posts', getPosts)

  if (status === 'loading') {
    return <div>loading...</div> // loading state
  }

  if (status === 'error') {
    return <div>{error.message}</div> // error state
  }

  return (
    <div>
      { data && <ul>{
        data
          .slice(0,10) // only take frist 10 for now
          .map(d => <li key={`post-${d.id}`}>
            <NavLink to={`/post/${d.id}`}>{d.title}</NavLink>
          </li>) // render list of titles
      }</ul> }
      { isFetching && <p>updating...</p> }
    </div>
  );
}

export default Home
```

Теперь давайте добавим в App.js маршрутизатор, который сопоставляет маршруты / для Home.js и /post/:id для одной страницы поста.

```jsx
import React from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'

import Home from './components/Home'
import Post from './components/Post'

function App() {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path = '/post/:id' render = {routerProps => <Post id={routerProps.match.params.id}/>} />
      </Switch>
    </Router>
  )
}

export default App
```

И наконец, я создам новый компонент components/Post.js для отображения данных одного поста. Объяснение будет следовать после кода.

```jsx
import React from 'react'
import { NavLink } from 'react-router-dom'
import { useQuery } from 'react-query'

const Post = ({ id }) => {
  const getPost = async () => {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
    const jsonResponse = await response.json()
    jsonResponse.title = `${jsonResponse.title} - ${Math.random().toString(36)}`

    await new Promise(r => setTimeout(r, 1000)) // wait a second
    return jsonResponse
  }

  const { status, data, isFetching } = useQuery(`post-${id}`, getPost)

  if (status === 'loading') {
    return <div>loading...</div> // loading state
  }

  return (
    <div>
      <h1>{data.title}</h1>
      <p>{data.body}</p>
      { isFetching && <p>updating...</p> }
      <br />
      <NavLink to="/">Home</NavLink>
    </div>
  )
}

export default Post
```

Таким образом, useQuery здесь не сильно отличается от того, что в Home.js. Он добавляет id к идентификатору, поэтому каждое сообщение имеет свой собственный state. Кроме того, я добавил тайм-аут на 1 секунду к функцию getPost, чтобы сделать состояние загрузки более заметным. Также я добавил случайную строку к заголовку, чтобы сделать повторную загрузку видимой.

И это на самом деле весь код GIF, которую вы видели в начале поста.

Если вы начнете работать с react-query, я бы порекомендовал вам посмотреть [react-query-devtools](https://github.com/tannerlinsley/react-query-devtools), чтобы иметь возможность просматривать state и кэш.