Как интегрировать RTK Query с Redux Toolkit: пошаговое руководство для разработчиков React

Redux - это библиотека управления состоянием приложений на JavaScript. Она позволяет создавать приложения, которые ведут себя предсказуемо и запускаются в различных средах, включая серверные и собственные среды. Redux Toolkit - это рекомендуемый способ написания логики Redux, созданный для упрощения работы с Redux.

Традиционно для написания логики Redux требовалось много шаблонного кода, настройки и установки зависимостей. Это затрудняло работу с Redux. RTK был создан для решения этих проблем. RTK содержит утилиты, которые упрощают общие задачи восстановления, такие как настройка хранилища, создание редукторов и логика обновления неизменяемого состояния.

Redux Toolkit Query (RTK Query) - это дополнительное дополнение, входящее в состав пакета Redux ToolKit. Оно было создано для упрощения получения и кэширования данных в веб-приложениях. RTK Query построен поверх Redux Toolkit и использует Redux для своего внутреннего архитектурного дизайна.

В этой статье вы узнаете, как интегрировать RTK Query с Redux Toolkit в свои приложения React, создав простое CRUD-приложение для просмотра фильмов.

содержание

  1. Предпосылки
  2. Понимание запросов RTK и основных концепций
  3. Интеграция RTK-запроса с Redux Toolkit
  4. Обработка кэширования данных с помощью RTK-запроса
  5. Состояния обработки ошибок и загрузки
  6. Лучшие практики
  7. Вывод

Предпосылки

Для этой статьи я предполагаю, что вы знакомы с React.

Понимание запросов RTK и основных концепций

В основе RTK Query лежит функция createApi. Эта функция позволяет вам определить фрагмент API, который включает базовый URL сервера и набор конечных точек, описывающих, как извлекать и изменять данные с сервера.

Запрос RTK автоматически генерирует пользовательские перехватчики для каждой из определенных конечных точек. Эти пользовательские перехватчики можно использовать в вашем компоненте React для условного отображения контента на основе состояния запроса API.

В приведенном ниже коде показано, как создать фрагмент API с помощью функции createApi:

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

export const apiSlice = createApi({
    reducerPath: 'api',
    baseQuery: fetchBaseQuery({ baseUrl: 'https://server.co/api/v1/'}),
    endpoints: (builder) => ({
        getData: builder.query({
            query: () => '/data',
        })
    })
})

export const { useGetDataQuery } = apiSlice;

fetchBaseQuery - это упрощенная оболочка для встроенной функции выборки JavaScript, которая упрощает запросы к API. Свойство reducerPath определяет каталог, в котором хранится фрагмент вашего API. Обычно для каталога api используется название. Свойство baseQuery использует функцию fetchBaseQuery для указания базового URL-адреса вашего сервера. Вы можете рассматривать его как корневой URL-адрес, к которому добавляются ваши конечные точки.

useGetDataQuery - это автоматически сгенерированный хук, который вы можете использовать в своих компонентах.

Как интегрировать RTK-запрос с Redux Toolkit

В этом разделе вы узнаете, как интегрировать RTK Query с Redux Toolkit, создав простое приложение для просмотра фильмов. В этом приложении пользователи смогут просматривать фильмы, хранящиеся в вашем серверной части (хотя это и макет серверной части), добавлять фильмы, а также обновлять и удалять любые фильмы. По сути, вы создадите CRUD-приложение, используя RTK-запрос.

Кроме того, в этом руководстве я буду использовать TypeScript. Если вы используете JavaScript, пропустите аннотации типов и/или интерфейсы и замените .tsx/.ts на .jsx/.js.

Настройка среды разработки

Создайте новый проект React, используя следующую команду:

npm create vite@latest

Следуйте инструкциям, чтобы создать свое приложение React.

Установите пакеты react-redux и @reduxjs/toolkit, используя следующую команду:

# npm
npm install @reduxjs/toolkit react-redux

# yarn
yarn add @reduxjs/toolkit react-redux

Для серверной части вы собираетесь использовать json-сервер. json-сервер - это легкий по весу Node.js инструмент, который имитирует RESTful API, используя файлы JSON в качестве источника данных. Это позволяет разработчикам интерфейсов создавать макеты API-интерфейсов без написания какого-либо серверного кода.

Подробнее о json-сервере вы можете прочитать здесь.

Используйте следующую команду для установки json-сервера:

npm install -g json-server

Структура папок

В корневом каталоге вашего приложения создайте папку с данными. Внутри этой папки создайте файл db.json. Именно здесь будет храниться ваш "сервер".

В каталоге src создайте две папки: component и state.

Внутри папки component создайте две папки: CardComponent и Modal, а также файл:Movies.tsx.

Внутри папки state создайте папку с фильмами и файл store.ts.

После создания папок и файлов структура вашего приложения должна выглядеть следующим образом:

Создание приложения

Во-первых, вы собираетесь настроить свой JSON-сервер.

Откройте файл db.json и вставьте в него следующий код:

{
  "movies": [
    {
      "title": "John Wick",
      "description": "Retired assassin John Wick is pulled back into the criminal underworld when gangsters kill his beloved dog, a gift from his late wife. With his unmatched combat skills and a thirst for vengeance, Wick single-handedly takes on an entire criminal syndicate.",
      "year": 2014,
      "thumbnail": "https://m.media-amazon.com/images/M/MV5BNTBmNWFjMWUtYWI5Ni00NGI2LWFjN2YtNDE2ODM1NTc5NGJlXkEyXkFqcGc@._V1_.jpg",
      "id": "2"
    },
    {
      "id": "3",
      "title": "The Dark Knight",
      "year": 2008,
      "description": "Batman faces off against his archenemy, the Joker, a criminal mastermind who plunges Gotham City into chaos. As the Joker tests Batman’s limits, the hero must confront his own ethical dilemmas to save the city from destruction.",
      "thumbnail": "https://m.media-amazon.com/images/M/MV5BMTMxNTMwODM0NF5BMl5BanBnXkFtZTcwODAyMTk2Mw@@._V1_FMjpg_UX1000_.jpg"
    },
    {
      "title": "Die Hard",
      "description": "NYPD officer John McClane finds himself in a deadly hostage situation when a group of terrorists takes control of a Los Angeles skyscraper during a Christmas party. Armed only with his wit and a handgun, McClane must outsmart the heavily armed intruders to save his wife and others.",
      "year": 1988,
      "thumbnail": "https://m.media-amazon.com/images/M/MV5BMGNlYmM1NmQtYWExMS00NmRjLTg5ZmEtMmYyYzJkMzljYWMxXkEyXkFqcGc@._V1_.jpg",
      "id": "4"
    },
    {
      "title": "Mission: Impossible – Fallout",
      "description": "Ethan Hunt and his IMF team must track down stolen plutonium while being hunted by assassins and former allies. With incredible stunts and non-stop action sequences, Hunt races against time to prevent a global catastrophe.",
      "year": 2018,
      "thumbnail": "https://m.media-amazon.com/images/M/MV5BMTk3NDY5MTU0NV5BMl5BanBnXkFtZTgwNDI3MDE1NTM@._V1_.jpg",
      "id": "5"
    },
    {
      "title": "Gladiator",
      "description": "Betrayed by the Emperor’s son and left for dead, former Roman General Maximus rises as a gladiator to seek vengeance and restore honor to his family. His journey from slavery to becoming a champion captures the hearts of Rome’s citizens.",
      "year": 2010,
      "thumbnail": "https://m.media-amazon.com/images/M/MV5BZmExODVmMjItNzFlZC00MDA0LWJkYjctMmQ0ZTNkYTcwYTMyXkEyXkFqcGc@._V1_.jpg",
      "id": "6"
    }
  ]
}

Запустите свой JSON-сервер с помощью следующей команды:

json-server --watch data\db.json --port 8080

Эта команда запустит ваш JSON-сервер и передаст конечную точку API, работающую на порту 8080. Ваш терминал должен выглядеть следующим образом:

Далее вы создадите фрагмент API. Этот фрагмент API будет использоваться для настройки вашего хранилища Redux.

Перейдите в папку "Фильмы" и создайте файл movieApiSlice.ts. Откройте файл Movieapislice.ts и вставьте в него следующий код:

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

export const moviesApiSlice = createApi({
  reducerPath: "movies",
  baseQuery: fetchBaseQuery({
    baseUrl: "http://localhost:8080",
  }),
  endpoints: (builder) => {
    return {
      getMovies: builder.query({
        query: () => `/movies`,
      }),

      addMovie: builder.mutation({
        query: (movie) => ({
          url: "/movies",
          method: "POST",
          body: movie,
        }),
      }),

      updateMovie: builder.mutation({
        query: (movie) => {
          const { id, ...body } = movie;
          return {
            url: `movies/${id}`,
            method: "PUT",
            body
          }
        },
      }),

      deleteMovie: builder.mutation({
        query: ({id}) => ({
          url: `/movies/${id}`,
          method: "DELETE",
          body: id,
        }),
      }),
    };
  },
});

export const {
  useGetMoviesQuery,
  useAddMovieMutation,
  useDeleteMovieMutation,
  useUpdateMovieMutation,
} = moviesApiSlice;

В приведенном выше коде вы создали movieApiSlice, используя функцию createApi из RTK Query, которая принимает объект в качестве параметра.

Свойство reducerPath определяет путь к фрагменту API.

Базовый запрос использует fetchBaseQuery. Функция fetchBaseQuery принимает в качестве параметра объект, который имеет свойство baseUrl. Свойство baseUrl определяет корневой URL нашего API.

В данном случае вы используете http://localhost:8080, который является URL-адресом JSON-сервера.

Свойство endpoints - это то, с чем взаимодействует ваш API. Это функция, которая принимает параметр builder и возвращает объект с методами (getMovies, addMovie, updateMovie и deleteMovie) для взаимодействия с вашим API.

Наконец, вы экспортируете пользовательские перехватчики, автоматически сгенерированные с помощью RTK Query. Пользовательский перехватчик начинается с "use" и заканчивается "query" и называется на основе методов, определенных в свойстве endpoints.

Эти пользовательские перехватчики позволяют вам взаимодействовать с API из ваших функциональных компонентов.

Далее вам нужно настроить хранилище Redux. Перейдите к файлу store.ts, расположенному в папке state, и вставьте следующий код:

import { configureStore } from "@reduxjs/toolkit";
import { moviesApiSlice } from "./movies/moviesApiSlice";

export const store = configureStore({
    reducer: {
        [moviesApiSlice.reducerPath]: moviesApiSlice.reducer,
    },
    middleware: (getDefaultMiddleware) => {
        return getDefaultMiddleware().concat(moviesApiSlice.middleware);
    }
})

В приведенном выше коде вы настраиваете хранилище Redux с помощью функции configureStore из Redux Toolkit. Свойство reducer определяет средство восстановления для обновления состояния в хранилище Redux. moviesApiSlice.reducer - это средство обновления состояния вашего API.

Для свойства middleware вы создаете промежуточное программное обеспечение для обработки асинхронных обновлений состояния. Вам не нужно слишком беспокоиться об этой части и о том, что она делает. Это необходимо для всех функций кэширования и всех других преимуществ, которые предоставляет RTK Query.

Прежде чем мы продолжим, вам необходимо добавить хранилище Redux в ваше приложение. Для этого перейдите к вашему файлу main.tsx или index.tsx (в зависимости от того, как он называется в вашем приложении) и замените код следующим кодом:

// main.tsx

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import { Provider } from "react-redux";
import { store } from "./state/store.ts";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>
);

В приведенном выше коде вы импортируете компонент Provider из react-redux и созданного ранее хранилища. Кроме того, вы добавляете компонент Provider в компонент вашего приложения. Prop магазина используется для передачи хранилища Redux в ваше приложение.

Создание компонента фильма

В этом разделе вы собираетесь создать компонент Movies.tsx, в котором сосредоточена вся логика вашего приложения.

Перейдите к вашему файлу Movies.tsx и вставьте следующий код:

import "../movie.css";
import { ChangeEvent, FormEvent, useState } from "react";

import {
  useGetMoviesQuery,
  useAddMovieMutation,
  useDeleteMovieMutation,
} from "../state/movies/moviesApiSlice";
import MovieCard from "./CardComponent/MovieCard";

export interface Movie {
  title: string;
  description: string;
  year: number;
  thumbnail: string;
  id: string;
}


export default function Movies() {
  // Form input states
  const [title, setTitle] = useState<string>("");
  const [year, setYear] = useState<string>("");
  const [thumbnail, setThumbnail] = useState<string>("");
  const [description, setDescription] = useState<string>("");

  const { data: movies = [], isLoading, isError } = useGetMoviesQuery({});

  const [ addMovie ] = useAddMovieMutation();
  const [ deleteMovie ] = useDeleteMovieMutation();

  // Handle form submission to add a new movie
  const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
    e.preventDefault();
    console.log("New movie submitted:", { title, thumbnail, description, year });
    addMovie({ title, description, year: Number(year), thumbnail, id: String(movies.length + 1) })
    // Reset form inputs after submission
    setTitle("");
    setThumbnail("");
    setDescription("");
    setYear("");
  };

  if (isError) {
    return <div>Error</div>;
  }

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

  return (
    <div className="movie-container">
      <h2>Movies to Watch</h2>

      {/* Form to add a new movie */}
      <div className="new-movie-form">
        <form onSubmit={handleSubmit}>
          <div className="form-group">
            <label htmlFor="title">Title</label>
            <input
              type="text"
              name="title"
              id="title"
              placeholder="Enter movie title"
              value={title}
              onChange={(e: ChangeEvent<HTMLInputElement>) => setTitle(e.target.value)}
              required
            />
          </div>

          <div className="form-group">
            <label htmlFor="imageAddress">Image Link:</label>
            <input
              type="text"
              name="imageAddress"
              id="imageAddress"
              placeholder="Enter image link address"
              value={thumbnail}
              onChange={(e: ChangeEvent<HTMLInputElement>) => setThumbnail(e.target.value)}
              required
            />
          </div>

          <div className="form-group">
            <label htmlFor="year">Year of release:</label>
            <input
              type="text"
              name="year"
              id="year"
              placeholder="Enter year of release"
              value={year}
              onChange={(e: ChangeEvent<HTMLInputElement>) => setYear(e.target.value)}
            />
          </div>

          <div className="form-group">
            <label htmlFor="description">Description</label>
            <textarea
              name="description"
              id="description"
              placeholder="Enter movie description"
              value={description}
              onChange={(e: ChangeEvent<HTMLTextAreaElement>) => setDescription(e.target.value)}
              required
            ></textarea>
          </div>

          <button type="submit">Add Movie</button>
        </form>
      </div>

      {/* Render list of movies */}
      <div className="movie-list">
        {movies.length === 0 ? (
          <p>No movies added yet.</p>
        ) : (
          movies.map((movie: Movie) => (
            <div key={movie.id}>
              <MovieCard movie={movie} deleteMovie={deleteMovie} />
            </div>
          ))
        )}
      </div>
    </div>
  );
}

В приведенном выше коде вы создаете компонент Movies и используете RTK-запрос для обработки CRUD-операций.

Давайте шаг за шагом рассмотрим, что делает каждая часть кода.

В верхней части вы импортировали функции useGetMoviesQuery, useAddMovieMutation и useDeleteMovie из созданного вами ранее приложения moviesApiSlice. Эти функции будут использоваться для извлечения, добавления и удаления фильмов соответственно.

Вы также импортировали компонент MovieCard, который будете использовать для отображения каждого фильма. Через секунду вы создадите компонент MovieCard.

Интерфейс Movie определяет форму каждого объекта movie. Это обеспечивает согласованность структуры данных о фильме в компоненте. Опять же, не обращайте внимания, если вы используете JavaScript.

Вы определили некоторые переменные состояния: название, год, миниатюру и описание для хранения входных значений формы.

Программа useGetMoviesQuery извлекает данные о фильме при монтаже компонента. Программа возвращает объект с несколькими свойствами, но мы сосредоточимся на трех свойствах: данные, которые называются фильмами, загружаются и ошибочны.

Перехватчики useAddMovieMutation и useDeleteMovieMutation возвращают две функции: addMovie и deleteMovie соответственно.

Функция handleSubmit обрабатывает отправку формы. Когда форма отправляется, вызывается функция addMovie с новыми сведениями о фильме. Год преобразуется в число, а идентификатор генерируется на основе текущей длины массива фильмов.

Если при загрузке фильмов возникает ошибка (isError), отображается простое сообщение об ошибке.

Если запрос API все еще загружается (isLoading), отображается сообщение о загрузке.

Если все идет хорошо, возвращается основная структура компонента в формате JSX, которая включает:

  • форма для добавления новых фильмов.
  • список фильмов, отрисованных с помощью компонента MovieCard. На каждую видеокарту передаются отдельные данные о фильме вместе с функцией deleteMovie для обработки удалений.

Теперь давайте создадим наш компонент MovieCard.

Внутри папки CardComponent создайте файл MovieCard.tsx. Откройте файл MovieCard.tsx и вставьте в него следующий код:

import { useRef, useState } from "react";
import EditModal from "../Modal/EditModal";
import { Movie } from "../Movies";

type DeleteMovie = (movie:{id:string}) => void;

interface MovieCardProps {
  movie: Movie;
  deleteMovie: DeleteMovie;
}

function MovieCard({ movie, deleteMovie }: MovieCardProps) {

  const dialogRef = useRef<HTMLDialogElement | null>(null);
  const [selectedMovie, setSelectedMovie] = useState<Movie>(movie);

  const handleSelectedMovie = () => {
    setSelectedMovie(movie);
    dialogRef.current?.showModal();
    document.body.style.overflow = 'hidden';
  }

  const closeDialog = (): void => {
    dialogRef.current?.close();
    document.body.style.overflow = 'visible';
  }

  return (
    <div className="movie-wrapper" key={movie.id}>
      <div className="img-wrapper">
        <img src={movie.thumbnail} alt={`${movie.title} poster`} />
      </div>
      <h3>
        {movie.title} ({movie.year})
      </h3>
      <p>{movie.description}</p>
      <div className="button-wrapper">
        <button onClick={handleSelectedMovie}>Edit</button>
        <button onClick={() => deleteMovie({ id: movie.id })}>Delete</button>
      </div>

      <EditModal dialogRef={dialogRef} selectedMovie={selectedMovie} closeDialog={closeDialog} />

    </div>
  );
}

export default MovieCard;

В приведенном выше коде вы создаете компонент MovieCard для отображения фильмов на экране.

Вы импортируете перехватчики useRef и useState из React для управления состоянием компонента и ссылками на него. Вы также импортируете компонент EditModal, который будет обрабатывать редактирование сведений о фильме, и тип MOVIE для изменения формы объекта movie (это для TypeScript).

Компонент MovieCard поддерживает два реквизита: movie и deleteMovie.

Переменная dialogRef используется для управления ссылкой на элемент модального диалога.

Состояние selectedMovie инициализируется с помощью параметра movie prop. Это будет использоваться для отслеживания текущего выбранного фильма в целях редактирования.

Функция handleSelectedMovie вызывается при нажатии кнопки редактирования. Она выполняет следующие действия:

  • Устанавливает selectedMovie в соответствие с текущим объектом movie.
  • Открывает диалоговое окно редактирования с помощью функции dialogRef.current?.ShowModal().
  • Предотвращает прокрутку страницы при открытом модальном режиме, установив для document.body.style.overflow значение "скрытый".

Функция closeDialog закрывает модальный диалог с помощью dialogRef.current?.close() и сбрасывает режим прокрутки страницы, устанавливая для document.body.style.overflow значение "видимый".

В инструкции return возвращается структура JSX, которая отображает:

  • изображение для миниатюры фильма,
  • название фильма и год выхода в формате h3,
  • краткое описание фильма,
  • две кнопки: Кнопка "Редактировать" запускает функцию handleSelectedMovie для открытия режима редактирования. Кнопка "Удалить" вызывает функцию deleteMovie, передавая идентификатор запроса фильма, чтобы удалить указанный фильм из вашего API.

Компонент EditModal также визуализируется, передавая dialogRef, closeDialog и selectedMovie в качестве реквизита. Это гарантирует, что у EditModal есть доступ к деталям выбранного фильма и функция самостоятельного закрытия.

Далее вы создадите компонент EditModal.

Внутри папки Modal создайте файл EditModal.tsx, в котором будет размещен модальный компонент.

Откройте файл EditModal.tsx и вставьте в него следующий код:

import { useUpdateMovieMutation } from "../../state/movies/moviesApiSlice";
import { Movie } from "../Movies";
import "./modal.css";
import { useState, RefObject, FormEvent } from "react";

interface EditModalProps {
  dialogRef: RefObject<HTMLDialogElement>;
  selectedMovie: Movie;
  closeDialog: () => void;
}

function EditModal({ dialogRef, selectedMovie, closeDialog }: EditModalProps) {
  const [title, setTitle] = useState<string>(selectedMovie.title);
  const [year, setYear] = useState<string | number>(selectedMovie.year);
  const [description, setDescription] = useState<string>(selectedMovie.description);
  const [thumbnail, setThumbnail] = useState<string>(selectedMovie.thumbnail);

  const [updateMovie] = useUpdateMovieMutation();

  async function handleUpdateMovie(e: FormEvent<HTMLFormElement>){
    e.preventDefault();
    try {
      await updateMovie({title, description, year: Number(year), thumbnail, id: selectedMovie.id});
      closeDialog();
    } catch (error) {
      alert(`${error} occurred`);
    }
  }

  return (
    <dialog ref={dialogRef} className="modal-dialog">
      <form onSubmit={handleUpdateMovie}>
        <div className="form-group">
          <label htmlFor="title">Title:</label>
          <input
            type="text"
            id="title"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
          />
        </div>

        <div className="form-group">
          <label htmlFor="year">Year of release:</label>
          <input
            type="text"
            id="year"
            value={year}
            onChange={(e) => setYear(e.target.value)}
          />
        </div>

        <div className="form-group">
          <label htmlFor="thumbnail">Image URL:</label>
          <input
            type="text"
            id="thumbnail"
            value={thumbnail}
            onChange={(e) => setThumbnail(e.target.value)}
          />
        </div>

        <div className="form-group">
          <label htmlFor="description">Description:</label>
          <textarea
            id="description"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
          ></textarea>
        </div>
        <button type="submit">Save</button>
      </form>
      <button className="close-btn" onClick={closeDialog}>
        Close
      </button>
    </dialog>
  );
}

export default EditModal;

В приведенном выше коде вы просто создаете модальный диалог, используя собственный HTML-элемент <dialog>. Внутри элемента dialog находится поле формы, заполненное подробной информацией о выбранном фильме, полученной из переменных состояния: название, год, описание и миниатюра.

Вы импортировали функцию useUpdateMovieMutation из своего приложения moviesApiSlice. Функция useUpdateMovieMutation возвращает функцию updateMovie, которую можно использовать для обновления сведений о фильме.

Программа handleUpdateMovie вызывается при нажатии кнопки Сохранить. Она выполняет следующие действия:

  • обновляет сведения о фильме, вызывая функцию updateMovie
  • закрывает диалоговое окно с помощью функции closeDialog

Монтаж нашего компонента

Перейдите к своему файлу App.tsx и добавьте в свой компонент Movies следующий код:

import "./App.css";
import Movies from "./components/Movies";

function App() {
  return (
    <div>
      <Movies />
    </div>
  );
}

export default App;

В вашем браузере откройте свой локальный хостинг, и вы должны увидеть что-то вроде этого:

Поздравляю! Вы успешно интегрировали RTK Query с Redux Toolkit.

В следующем разделе вы узнаете, как работает кэширование в RTK Query и как сделать кэши недействительными.

Как обрабатывать кэширование данных с помощью RTK-запроса

В этом разделе вы узнаете, как работает кэширование в RTK Query и как сделать кэши недействительными.

В программировании кэширование - одна из самых сложных задач. Но RTK Query упрощает нам работу с кэшированием.

Когда вы вызываете свой API, RTK Query автоматически кэширует результат успешного вызова вашего API. Это означает, что последующие вызовы API возвращают кэшированный результат.

Например, если вы попытаетесь отредактировать какой-либо фильм в своем приложении, вы заметите, что ничего не меняется. Это не значит, что оно не работает - на самом деле, оно работает. И возвращаемые результаты - это кэшированная версия (результаты, полученные при первом вызове API, то есть при монтировании компонента).

Чтобы остановить такое поведение, вам необходимо аннулировать кэш каждый раз, когда вы вносите изменения в свой сервер. Это приведет к тому, что RTK Query автоматически выполнит повторную выборку данных, чтобы отразить ваши изменения.

Перейдите к своему файлу moviesApiSlice.ts и замените этот код следующим кодом:


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

export const moviesApiSlice = createApi({
  reducerPath: "movies",
  baseQuery: fetchBaseQuery({
    baseUrl: "http://localhost:8080",
  }),
  tagTypes: ['Movies'],
  endpoints: (builder) => {
    return {
      getMovies: builder.query({
        query: () => `/movies`,
        providesTags: ['Movies']
      }),

      addMovie: builder.mutation({
        query: (movie) => ({
          url: "/movies",
          method: "POST",
          body: movie,
        }),
        invalidatesTags: ['Movies']
      }),

      updateMovie: builder.mutation({
        query: (movie) => {
          const { id, ...body } = movie;
          return {
            url: `movies/${id}`,
            method: "PUT",
            body
          }
        },
        invalidatesTags: ['Movies']
      }),

      deleteMovie: builder.mutation({
        query: ({id}) => ({
          url: `/movies/${id}`,
          method: "DELETE",
          body: id,
        }),
        invalidatesTags: ['Movies']
      }),
    };
  },
});

export const {
  useGetMoviesQuery,
  useAddMovieMutation,
  useDeleteMovieMutation,
  useUpdateMovieMutation,
} = moviesApiSlice;

В приведенном выше коде вы добавили свойство tagTypes в свой moviesApiSlice и присвоили ему значение[Фильмы]. Это будет использоваться для аннулирования кэшированных результатов при внесении изменений в ваш сервер.

В функцию getMovies вы добавили свойство providesTags. Это означает, что вы предоставляете тег для своего вызова API, который можно аннулировать с помощью функций мутации.

В функциях изменения (addMovie, updateMovie и deleteMovie) вы добавили свойство invalidatesTags, установленное в значение свойства tagTypes. Это приводит к аннулированию кэша при каждом вызове каждой из этих функций мутации, что приводит к автоматической повторной выборке данных из запроса RTK.

С помощью этих изменений вы можете обновлять и удалять фильмы и просматривать результат внесенных изменений.

Состояния обработки ошибок и загрузки

Когда вы создавали свое приложение, вы обрабатывали любые ошибки, которые могли возникнуть при вызове вашего API, просто отображая текст "Ошибка...".

В реальных приложениях вы хотите отображать что-то значимое, например пользовательский интерфейс, который сообщает вашим пользователям, что именно пошло не так.

Аналогично, когда загружается ваш запрос API, вы хотите отобразить индикатор загрузки или пользовательский интерфейс скелета загрузки, чтобы ваши пользователи знали, что данные вашего приложения загружаются.

В рамках этой статьи мы не собираемся углубляться в расширенную обработку ошибок или управление состояниями загрузки, но это те вопросы, на которые вы хотели бы обратить внимание.

Лучшие практики

Ниже приведены некоторые из рекомендаций, которые следует учитывать при работе с RTK-запросом:

  1. Разделите несколько фрагментов API: если у вас есть несколько фрагментов API для разных API-интерфейсов, рассмотрите возможность разделения их на разные фрагменты API. Это делает ваши фрагменты API модульными, что упрощает их обслуживание и отладку.
  2. Используйте Redux Devtools: Redux Devtools позволяют вам получить представление о том, что происходит в вашем хранилище Redux, а также о ваших запросах и изменениях. Это значительно упрощает отладку. Redux Devtools доступны в виде расширения для Chrome.
  3. Предварительная выборка данных: используйте функцию usePrefetch, чтобы выполнить выборку данных до того, как пользователь перейдет на страницу вашего веб-сайта или загрузит какой-либо известный контент. Это сокращает время загрузки и ускоряет работу пользовательского интерфейса.
  4. Используйте промежуточное программное обеспечение для сложной логики: внедряйте промежуточное программное обеспечение, когда вам нужно перехватывать и изменять действия или ответы, например, добавлять токены аутентификации в заголовки или регистрировать определенные ошибки.
  5. Используйте оптимистичные обновления: при использовании useMutation для обновления или изменения существующих данных вы можете реализовать оптимистичное обновление пользовательского интерфейса. Это помогает создать впечатление немедленных изменений. Если запрос не выполняется, вы можете отменить обновление.

Вывод

В этой статье вы узнали, что такое RTK Query и как интегрировать RTK Query с Redux Toolkit, создав приложение CRUD React Movie. Вы также узнали о стратегиях кэширования RTK Query и о том, как аннулировать кэши.

Спасибо, что прочитали!