← React/RTK Query: автоматический data fetching#304 из 383← ПредыдущийСледующий →+35 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

RTK Query: автоматический data fetching

Зачем RTK Query

createAsyncThunk требует много boilerplate: состояния loading/error, кэширование, refetch... RTK Query автоматизирует всё это.

RTK Query vs createAsyncThunk:

| Функция | createAsyncThunk | RTK Query |

|---------|------------------|-----------|

| Loading состояние | Вручную | Автоматически |

| Кэширование | Вручную | Автоматически |

| Дедупликация | Вручную | Автоматически |

| Refetch | Вручную | Автоматически |

| Оптимистичные обновления | Сложно | Встроено |

| Invalidation | Вручную | Теги |

Создание API

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Post', 'User'],
  endpoints: (builder) => ({
    // Query — GET запросы
    getPosts: builder.query({
      query: () => '/posts',
      providesTags: ['Post']
    }),
    getPostById: builder.query({
      query: (id) => `/posts/${id}`,
      providesTags: (result, error, id) => [{ type: 'Post', id }]
    }),

    // Mutation — POST/PUT/DELETE
    createPost: builder.mutation({
      query: (newPost) => ({
        url: '/posts',
        method: 'POST',
        body: newPost
      }),
      invalidatesTags: ['Post']  // Автоматически refetch getPosts
    }),
    deletePost: builder.mutation({
      query: (id) => ({
        url: `/posts/${id}`,
        method: 'DELETE'
      }),
      invalidatesTags: (result, error, id) => [{ type: 'Post', id }]
    })
  })
})

// Автогенерированные хуки
export const {
  useGetPostsQuery,
  useGetPostByIdQuery,
  useCreatePostMutation,
  useDeletePostMutation
} = api

export default api

Подключение к store

import { configureStore } from '@reduxjs/toolkit'
import api from './api'

const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
    // другие редьюсеры
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware)
})

Использование Query хуков

function PostsList() {
  const {
    data: posts,    // Данные
    isLoading,      // Первая загрузка
    isFetching,     // Любая загрузка (включая refetch)
    isSuccess,      // Успех
    isError,        // Ошибка
    error,          // Объект ошибки
    refetch         // Функция для повторного запроса
  } = useGetPostsQuery()

  if (isLoading) return <Spinner />
  if (isError) return <Error message={error.message} />

  return (
    <div>
      {isFetching && <RefetchIndicator />}
      {posts.map(post => <PostCard key={post.id} post={post} />)}
      <button onClick={refetch}>Обновить</button>
    </div>
  )
}

Query с параметрами

function PostPage({ postId }) {
  const { data: post, isLoading } = useGetPostByIdQuery(postId)

  // Пропуск запроса если нет id
  const { data } = useGetPostByIdQuery(postId, {
    skip: !postId
  })

  // Polling каждые 30 секунд
  const { data: liveData } = useGetPostsQuery(undefined, {
    pollingInterval: 30000
  })
}

Mutations

function CreatePostForm() {
  const [createPost, { isLoading, isSuccess, isError }] = useCreatePostMutation()

  const handleSubmit = async (formData) => {
    try {
      await createPost(formData).unwrap()
      // Успех! getPosts автоматически обновится благодаря invalidatesTags
    } catch (error) {
      console.error('Ошибка:', error)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {isLoading && <span>Сохранение...</span>}
      {isSuccess && <span>Сохранено!</span>}
      {isError && <span>Ошибка!</span>}
      <button type="submit" disabled={isLoading}>
        Создать пост
      </button>
    </form>
  )
}

Теги и кэш-инвалидация

endpoints: (builder) => ({
  getUsers: builder.query({
    query: () => '/users',
    providesTags: (result) =>
      result
        ? [
            ...result.map(({ id }) => ({ type: 'User', id })),
            { type: 'User', id: 'LIST' }
          ]
        : [{ type: 'User', id: 'LIST' }]
  }),
  updateUser: builder.mutation({
    query: ({ id, ...patch }) => ({
      url: `/users/${id}`,
      method: 'PATCH',
      body: patch
    }),
    // Инвалидирует только конкретного пользователя
    invalidatesTags: (result, error, { id }) => [{ type: 'User', id }]
  }),
  deleteUser: builder.mutation({
    query: (id) => ({
      url: `/users/${id}`,
      method: 'DELETE'
    }),
    // Инвалидирует весь список
    invalidatesTags: [{ type: 'User', id: 'LIST' }]
  })
})

Оптимистичные обновления

updatePost: builder.mutation({
  query: ({ id, ...patch }) => ({
    url: `/posts/${id}`,
    method: 'PATCH',
    body: patch
  }),
  async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
    // Оптимистично обновляем кэш
    const patchResult = dispatch(
      api.util.updateQueryData('getPosts', undefined, (draft) => {
        const post = draft.find(p => p.id === id)
        if (post) Object.assign(post, patch)
      })
    )
    try {
      await queryFulfilled
    } catch {
      // Откатываем при ошибке
      patchResult.undo()
    }
  }
})

Примеры

Симуляция RTK Query: автоматические хуки для data fetching

// Упрощённая симуляция RTK Query
function createApi({ baseUrl, endpoints }) {
  const cache = new Map()
  const subscribers = new Map()

  const endpointDefs = endpoints({
    query: (config) => ({ type: 'query', ...config }),
    mutation: (config) => ({ type: 'mutation', ...config })
  })

  const hooks = {}

  Object.entries(endpointDefs).forEach(([name, def]) => {
    if (def.type === 'query') {
      // Создаём useXxxQuery хук (симуляция)
      hooks['use' + name.charAt(0).toUpperCase() + name.slice(1) + 'Query'] = (arg) => {
        const cacheKey = name + ':' + JSON.stringify(arg)
        return cache.get(cacheKey) || { isLoading: true, data: undefined }
      }
    }
  })

  return { hooks, endpointDefs, cache }
}

// === Создаём API ===
const postsApi = createApi({
  baseUrl: '/api',
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => '/posts',
      providesTags: ['Post']
    }),
    getPostById: builder.query({
      query: (id) => '/posts/' + id,
      providesTags: (result, error, id) => [{ type: 'Post', id }]
    }),
    createPost: builder.mutation({
      query: (newPost) => ({
        url: '/posts',
        method: 'POST',
        body: newPost
      }),
      invalidatesTags: ['Post']
    })
  })
})

console.log('=== RTK Query Concepts ===\n')

console.log('Созданные endpoints:')
Object.entries(postsApi.endpointDefs).forEach(([name, def]) => {
  console.log('  ' + name + ':', def.type, def.query ? def.query(123) : def.mutation)
})

console.log('\nАвтогенерированные хуки:')
Object.keys(postsApi.hooks).forEach(hook => {
  console.log('  ' + hook)
})

console.log('\n--- Как работает RTK Query ---')
console.log(`
1. Определяем endpoints (query/mutation)
2. RTK Query генерирует:
   - useGetPostsQuery() — хук для получения всех постов
   - useGetPostByIdQuery(id) — хук для получения поста по ID
   - useCreatePostMutation() — хук для создания поста

3. Хуки автоматически:
   - Кэшируют данные
   - Отслеживают loading/error состояния
   - Дедуплицируют одинаковые запросы
   - Инвалидируют кэш через теги
`)

// Демонстрация использования
console.log('\n--- Пример использования в компоненте ---')
console.log(`
function PostsList() {
  const { data, isLoading, error } = useGetPostsQuery()

  if (isLoading) return <Loading />
  if (error) return <Error />

  return data.map(post => <Post key={post.id} {...post} />)
}
`)

RTK Query: автоматический data fetching

Зачем RTK Query

createAsyncThunk требует много boilerplate: состояния loading/error, кэширование, refetch... RTK Query автоматизирует всё это.

RTK Query vs createAsyncThunk:

| Функция | createAsyncThunk | RTK Query |

|---------|------------------|-----------|

| Loading состояние | Вручную | Автоматически |

| Кэширование | Вручную | Автоматически |

| Дедупликация | Вручную | Автоматически |

| Refetch | Вручную | Автоматически |

| Оптимистичные обновления | Сложно | Встроено |

| Invalidation | Вручную | Теги |

Создание API

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Post', 'User'],
  endpoints: (builder) => ({
    // Query — GET запросы
    getPosts: builder.query({
      query: () => '/posts',
      providesTags: ['Post']
    }),
    getPostById: builder.query({
      query: (id) => `/posts/${id}`,
      providesTags: (result, error, id) => [{ type: 'Post', id }]
    }),

    // Mutation — POST/PUT/DELETE
    createPost: builder.mutation({
      query: (newPost) => ({
        url: '/posts',
        method: 'POST',
        body: newPost
      }),
      invalidatesTags: ['Post']  // Автоматически refetch getPosts
    }),
    deletePost: builder.mutation({
      query: (id) => ({
        url: `/posts/${id}`,
        method: 'DELETE'
      }),
      invalidatesTags: (result, error, id) => [{ type: 'Post', id }]
    })
  })
})

// Автогенерированные хуки
export const {
  useGetPostsQuery,
  useGetPostByIdQuery,
  useCreatePostMutation,
  useDeletePostMutation
} = api

export default api

Подключение к store

import { configureStore } from '@reduxjs/toolkit'
import api from './api'

const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
    // другие редьюсеры
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware)
})

Использование Query хуков

function PostsList() {
  const {
    data: posts,    // Данные
    isLoading,      // Первая загрузка
    isFetching,     // Любая загрузка (включая refetch)
    isSuccess,      // Успех
    isError,        // Ошибка
    error,          // Объект ошибки
    refetch         // Функция для повторного запроса
  } = useGetPostsQuery()

  if (isLoading) return <Spinner />
  if (isError) return <Error message={error.message} />

  return (
    <div>
      {isFetching && <RefetchIndicator />}
      {posts.map(post => <PostCard key={post.id} post={post} />)}
      <button onClick={refetch}>Обновить</button>
    </div>
  )
}

Query с параметрами

function PostPage({ postId }) {
  const { data: post, isLoading } = useGetPostByIdQuery(postId)

  // Пропуск запроса если нет id
  const { data } = useGetPostByIdQuery(postId, {
    skip: !postId
  })

  // Polling каждые 30 секунд
  const { data: liveData } = useGetPostsQuery(undefined, {
    pollingInterval: 30000
  })
}

Mutations

function CreatePostForm() {
  const [createPost, { isLoading, isSuccess, isError }] = useCreatePostMutation()

  const handleSubmit = async (formData) => {
    try {
      await createPost(formData).unwrap()
      // Успех! getPosts автоматически обновится благодаря invalidatesTags
    } catch (error) {
      console.error('Ошибка:', error)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {isLoading && <span>Сохранение...</span>}
      {isSuccess && <span>Сохранено!</span>}
      {isError && <span>Ошибка!</span>}
      <button type="submit" disabled={isLoading}>
        Создать пост
      </button>
    </form>
  )
}

Теги и кэш-инвалидация

endpoints: (builder) => ({
  getUsers: builder.query({
    query: () => '/users',
    providesTags: (result) =>
      result
        ? [
            ...result.map(({ id }) => ({ type: 'User', id })),
            { type: 'User', id: 'LIST' }
          ]
        : [{ type: 'User', id: 'LIST' }]
  }),
  updateUser: builder.mutation({
    query: ({ id, ...patch }) => ({
      url: `/users/${id}`,
      method: 'PATCH',
      body: patch
    }),
    // Инвалидирует только конкретного пользователя
    invalidatesTags: (result, error, { id }) => [{ type: 'User', id }]
  }),
  deleteUser: builder.mutation({
    query: (id) => ({
      url: `/users/${id}`,
      method: 'DELETE'
    }),
    // Инвалидирует весь список
    invalidatesTags: [{ type: 'User', id: 'LIST' }]
  })
})

Оптимистичные обновления

updatePost: builder.mutation({
  query: ({ id, ...patch }) => ({
    url: `/posts/${id}`,
    method: 'PATCH',
    body: patch
  }),
  async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
    // Оптимистично обновляем кэш
    const patchResult = dispatch(
      api.util.updateQueryData('getPosts', undefined, (draft) => {
        const post = draft.find(p => p.id === id)
        if (post) Object.assign(post, patch)
      })
    )
    try {
      await queryFulfilled
    } catch {
      // Откатываем при ошибке
      patchResult.undo()
    }
  }
})

Примеры

Симуляция RTK Query: автоматические хуки для data fetching

// Упрощённая симуляция RTK Query
function createApi({ baseUrl, endpoints }) {
  const cache = new Map()
  const subscribers = new Map()

  const endpointDefs = endpoints({
    query: (config) => ({ type: 'query', ...config }),
    mutation: (config) => ({ type: 'mutation', ...config })
  })

  const hooks = {}

  Object.entries(endpointDefs).forEach(([name, def]) => {
    if (def.type === 'query') {
      // Создаём useXxxQuery хук (симуляция)
      hooks['use' + name.charAt(0).toUpperCase() + name.slice(1) + 'Query'] = (arg) => {
        const cacheKey = name + ':' + JSON.stringify(arg)
        return cache.get(cacheKey) || { isLoading: true, data: undefined }
      }
    }
  })

  return { hooks, endpointDefs, cache }
}

// === Создаём API ===
const postsApi = createApi({
  baseUrl: '/api',
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => '/posts',
      providesTags: ['Post']
    }),
    getPostById: builder.query({
      query: (id) => '/posts/' + id,
      providesTags: (result, error, id) => [{ type: 'Post', id }]
    }),
    createPost: builder.mutation({
      query: (newPost) => ({
        url: '/posts',
        method: 'POST',
        body: newPost
      }),
      invalidatesTags: ['Post']
    })
  })
})

console.log('=== RTK Query Concepts ===\n')

console.log('Созданные endpoints:')
Object.entries(postsApi.endpointDefs).forEach(([name, def]) => {
  console.log('  ' + name + ':', def.type, def.query ? def.query(123) : def.mutation)
})

console.log('\nАвтогенерированные хуки:')
Object.keys(postsApi.hooks).forEach(hook => {
  console.log('  ' + hook)
})

console.log('\n--- Как работает RTK Query ---')
console.log(`
1. Определяем endpoints (query/mutation)
2. RTK Query генерирует:
   - useGetPostsQuery() — хук для получения всех постов
   - useGetPostByIdQuery(id) — хук для получения поста по ID
   - useCreatePostMutation() — хук для создания поста

3. Хуки автоматически:
   - Кэшируют данные
   - Отслеживают loading/error состояния
   - Дедуплицируют одинаковые запросы
   - Инвалидируют кэш через теги
`)

// Демонстрация использования
console.log('\n--- Пример использования в компоненте ---')
console.log(`
function PostsList() {
  const { data, isLoading, error } = useGetPostsQuery()

  if (isLoading) return <Loading />
  if (error) return <Error />

  return data.map(post => <Post key={post.id} {...post} />)
}
`)

Задание

Создай приложение с RTK Query паттерном: список пользователей с загрузкой, добавлением и удалением. Реализуй кэширование и автоматическое обновление списка после мутаций.

Подсказка

Для инвалидации кэша используй setVersion(v => v + 1). Это заставит useQuery перезапросить данные.

Загружаем среду выполнения...
Загружаем AI-помощника...