Redux یک کتابخانه مدیریت state برای اپلیکیشن‌های جاوااسکریپت است. این کتابخانه به ما اجازه می‌دهد تا اپلیکیشن‌هایی بسازیم که به صورت قابل پیش‌بینی عمل می‌کنند و در محیط‌های مختلف، از جمله سرور و محیط‌های native، اجرا می‌شوند. Redux Toolkit روش پیشنهادی برای نوشتن منطق Redux است و برای ساده‌تر کردن کار با Redux ایجاد شده است.

به‌طور سنتی، نوشتن منطق Redux نیاز به مقدار زیادی کد اولیه، پیکربندی و نصب وابستگی‌ها داشت. این موضوع کار با Redux را دشوار می‌کرد. RTK برای حل این مشکلات ایجاد شد. RTK شامل ابزارهایی است که وظایف رایج Redux مانند پیکربندی store، ایجاد reducerها و به‌روزرسانی state را به صورت immutable ساده‌تر می‌کند.

Redux Toolkit Query (RTK Query) یک افزونه اختیاری است که در پکیج Redux Toolkit گنجانده شده است. این افزونه برای ساده‌سازی دریافت و ذخیره‌سازی داده‌ها در اپلیکیشن‌های وب طراحی شده است. RTK Query بر پایه Redux Toolkit ساخته شده و از Redux برای طراحی معماری داخلی خود استفاده می‌کند.

در این مقاله، یاد می‌گیریم که چگو

نه RTK Query را در اپلیکیشن‌های React خود با Redux Toolkit ادغام کنیم. برای این کار، یک اپلیکیشن ساده CRUD برای مدیری

ت فیلم‌ها خواهیم ساخت.

درک RTK Query و مفاهیم اصلی

هسته اصلی RTK Query تابع createApi است. این تابع به ما اجازه می‌دهد که یک API slice تعریف کنیم. API slice شامل آدرس پایه سرور و مجموعه‌ای از endpointها است که نحوه دریافت و تغییر داده‌ها را از سرور مشخص می‌کنند.

RTK Query به صورت خودکار یک هوک سفارشی برای هر یک از endpointهای تعریف شده تولید می‌کند. این هوک‌ها را می‌توانیم در کامپوننت‌های React برای نمایش محتوای مورد نظر بر اساس وضعیت درخواست API استفاده کنیم.

کد زیر نحوه ایجاد یک API slice با استفاده از 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 یک wrapper سبک برای تابع fetch جاوااسکریپت است که درخواست‌های API را ساده‌تر می‌کند. ویژگی reducerPath دایرکتوری ذخیره API slice ما را مشخص می‌کند که معمولاً با نام api تعریف می‌شود. ویژگی baseQuery از تابع fetchBaseQuery برای تعیین آدرس پایه سرور استفاده می‌کند. این آدرس را می‌توانیم به عنوان ریشه‌ای در نظر بگیریم که سایر  endpointها به آن اضافه می‌شوند.

useGetDataQuery یک هوک auto-generated است که می‌توانیم آن را در کامپوننت‌ها مورد استفاده قرار دهیم.

چگونه RTK Query را با Redux Toolkit ادغام کنیم؟

در این بخش، نحوه ادغام RTK Query با Redux Toolkit را با ساخت یک اپلیکیشن ساده مدیریت فیلم‌ها یاد می‌گیریم. در این اپلیکیشن، کاربران می‌توانند فیلم‌های ذخیره شده در بک‌اند را مشاهده کنند (البته این یک بک‌اند شبیه‌سازی شده خواهد بود)، فیلم‌های جدید اضافه کنند و همچنین فیلم‌ها را به‌روزرسانی یا حذف نمایند. به طور کلی، یک اپلیکیشن CRUD با استفاده از RTK Query خواهیم ساخت.

همچنین در این آموزش از تایپ اسکریپت استفاده خواهیم کرد. اگر بخواهیم از جاوااسکریپپت استفاده کنیم، می‌توانیم تایپ‌ها و interfaceها را حذف کنیم و ..tsx/.ts را با ..jsx/.js جایگزین نماییم.

راه‌اندازی محیط توسعه

یک پروژه جدید React را با استفاده از دستور زیر ایجاد می‌کنیم:

npm create vite@latest

سپس، پکیج‌های react-redux و @reduxjs/toolkit را با استفاده از دستور زیر نصب می‌کنیم:

# npm
npm install @reduxjs/toolkit react-redux

# yarn
yarn add @reduxjs/toolkit react-redux

برای بخش بک‌اند، از json-server استفاده می‌کنیم. json-server یک ابزار سبک در Node.js است که با استفاده از فایل‌های JSON، یک API شبیه‌سازی شده RESTful ایجاد می‌کند. این ابزار به توسعه‌دهندگان فرانت‌اند این امکان را می‌دهد که بدون نیاز به نوشتن کد سمت سرور، یک API آزمایشی ایجاد کنند.

برای اطلاعات بیشتر درباره json-server، مطالعه مستندات آن می‌تواند مفید باشد.

با استفاده از دستور زیر، json-server را نصب می‌کنیم:

npm install -g json-server

ساختار پوشه‌ها

در دایرکتوری root برنامه، یک پوشه به نام data ایجاد می‌کنیم. درون این پوشه، یک فایل به نام db.json می‌سازیم. این فایل محلی خواهد بود که «بک‌اند» ما در آن ذخیره می‌شود.

در دایرکتوری src، دو پوشه component و state ایجاد می‌کنیم.

داخل پوشه component، دو پوشه CardComponent و Modal و یک فایل به نام Movies.tsx ایجاد می‌کنیم.

داخل پوشه state نیز، یک پوشه به نام movies و یک فایل به نام store.ts می‌سازیم.

پس از ایجاد این پوشه‌ها و فایل‌ها، ساختار برنامه ما باید به شکل زیر باشد:

MY-APP
│-- data
│   └── db.json
│-- node_modules
│-- public
│-- src
│   │-- assets
│   │-- component
│   │   │-- CardComponent
│   │   │-- Modal
│   │   └── Movies.tsx
│   │-- state
│   │   └── movies
│   │       └── store.ts
│   │-- App.css
│   │-- App.tsx
│   │-- index.css
│   │-- main.tsx
│   └── vite-env.d.ts
│-- .gitignore

ساخت برنامه

ابتدا باید JSON Server را راه‌اندازی کنیم:

فایل 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 Server را با استفاده از دستور زیر اجرا می‌کنیم:

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

این دستور، JSON Server را اجرا کرده و یک API Endpoint را روی پورت ۸۰۸۰ راه‌اندازی می‌کند.

در مرحله‌ی بعد، یک API Slice ایجاد می‌کنیم. این API Slice برای پیکربندی Redux store استفاده می‌شود.

ایجاد API Slice

به پوشه‌ی movies رفته و یک فایل به نام 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 ایجاد شده است که یک آبجکت را به عنوان پارامتر دریافت می‌کند.

این هوک‌های سفارشی به ما این امکان را می‌دهند که از داخل کامپوننت‌های  فانکشنال با API تعامل داشته باشیم.

تنظیم Redux Store

در این مرحله‌، باید Redux Store خود را تنظیم کنیم. به فایل 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 store با استفاده از تابع configureStore از Redux Toolkit راه‌اندازی کرده‌ایم.

قبل از ادامه‌ی کار، باید Redux store را به برنامه‌ی خود اضافه نماییم.

برای این کار، به فایل 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>
);

در کد بالا:

ساخت کامپوننت Movie

در این بخش، قصد داریم تا کامپوننت 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 Query برای مدیریت عملیات CRUD بهره‌مند می‌شود.

بررسی گام‌به‌گام کد

ابتدا توابع useGetMoviesQuery، useAddMovieMutation و useDeleteMovie را از moviesApiSlice import می‌کنیم. این توابع برای دریافت، اضافه کردن و حذف فیلم‌ها استفاده می‌شوند.

کامپوننت MovieCard را نیز import می‌کنیم، که برای نمایش هر فیلم مورد استفاده قرار می‌گیرد. این کامپوننت را در مرحله بعد ایجاد خواهیم کرد.

اینترفیس Movie برای تعریف ساختار داده‌های فیلم استفاده شده است. اگر در پروژه خود از جاوااسکریپت استفاده می‌کنیم این قسمت را نادیده می‌گیریم.

متغیرهای state تعریف می‌کنیم که شامل title، year، thumbnail و description برای ذخیره مقادیر فرم هستند.

هوک useGetMoviesQuery داده‌های فیلم را هنگام بارگذاری کامپوننت fetch می‌کند و سه مقدار data (با نام مستعار movies)،  isLoading و isError را return می‌کند.

دو هوک useAddMovieMutation و useDeleteMovieMutation، توابع addMovie و deleteMovie را return می‌کنند که به ترتیب برای افزودن و حذف فیلم‌ها مورد استفاده قرار می‌گیرند.

تابع handleSubmit ارسال فرم را کنترل می‌کند. هنگامی که فرم ارسال می شود، تابع addMovie را برای افزودن فیلم جدید فراخوانی می‌کند. مقدار year را به عدد تبدیل کرده و id را بر اساس طول آرایه فیلم‌ها تولید می‌کند.

برای مدیریت خطاها و وضعیت بارگذاری:

اگر isError مقدار true داشته باشد، پیام خطا نمایش داده می‌شود.

اگر isLoading مقدار a-enlighter-language="generic">true داشته باشد، پیام «Loading...» نمایش داده می‌شود.

در نهایت، اگر همه چیز به درستی پیش برود ساختار JSX بازگردانده می‌شود که شامل موارد زیر است:

ساخت MovieCard

در پوشه CardComponent، یک فایل جدید به نام 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 برای مدیریت state و رفرنس‌های کامپوننت استفاده می‌کند.

کامپوننت EditModal را import می‌کنیم که برای ویرایش اطلاعات فیلم مورد استفاده قرار می‌گیرد.

MovieCard دو prop دریافت می‌کند:

مدیریت نمایش Modal:

تابع handleSelectedMovie با کلیک روی دکمه Edit فراخوانی می‌شود و کارهای زیر را انجام می‌دهد:

تابع closeDialog با استفاده از dialogRef.current?.close() Modal باز را می بندد و با تنظیم document.body.style.overflow به 'visible' رفتار اسکرول صفحه را مجددا فعال می‌کند.

در دستور return، یک ساختار JSX بازگردانده می‌شود که:

کامپوننت EditModal در اینجا نمایش داده شده و dialogRef، closeDialog و selectedMovie را دریافت می‌کند.

ساخت EditModal

در پوشه Modal، فایلی به نام 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;

کدی که داریم کارهای زیر را انجام می‌دهد:

Mounting کامپوننت ما

به فایل App.tsx می‌رویم و کامپوننت Movies را با استفاده از کد زیر، به آن اضافه می‌کنیم:

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

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

export default App;

تا این بخش از مقاله توانستیم با موفقیت RTK Query را با Redux Toolkit ادغام کنیم.

چگونه Caching داده‌ها را با RTK Query مدیریت کنیم؟

در این بخش، یاد می‌گیریم که Caching در RTK Query چگونه کار می‌کند و چطور می‌توانیم کش‌ها را invalidate کنیم.

در برنامه‌نویسی، Caching یکی از سخت‌ترین مفاهیم است. اما RTK Query این کار را برای ما ساده‌تر می‌کند.

هنگامی که API خود را فراخوانی می‌کنیم، RTK Query به طور خودکار نتیجه درخواست موفقیت‌آمیز را در کش ذخیره می‌کند. این یعنی برای درخواست‌های بعدی به همان API، نتیجه کش شده را return می‌کند.

برای مثال، اگر یک فیلم را در اپلیکیشن خود ویرایش کنیم، ممکن است متوجه شویم که تغییری در ظاهر داده‌ها ایجاد نشده است. این به این معنی نیست که عملیات ویرایش کار نمی‌کند، بلکه نتیجه‌ای که دریافت می‌کنیم همان نسخه کش شده می‌باشد.

برای جلوگیری از این رفتار، باید هر بار که تغییری در بک‌اند ایجاد می‌کنیم، کش را invalidate نماییم. این کار باعث می‌شود 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 اضافه می‌کنیم و مقدار آن را [Movies] تنظیم می‌نماییم. این ویژگی امکان invalidate کردن نتایج کش شده را هنگام اعمال تغییرات در بک‌اند فراهم می‌کند.

در تابع getMovies، ویژگی providesTags را اضافه می‌کنیم. این ویژگی باعث می‌شود که API درخواست‌ها را با یک برچسب مشخص کش کند، که می‌توانیم هنگام انجام عملیات mutation  آن را invalidate کنیم.

در توابع addMovie، updateMovie و deleteMovie، ویژگی invalidatesTags را با مقدار tagTypes تنظیم می‌کنیم. این ویژگی، کش را هر بار که این توابع mutation فراخوانی شوند، invalidate می‌کند و باعث می‌شود RTK Query به طور خودکار داده‌ها را دوباره دریافت کند.

با این تغییرات، اکنون می‌توانیم فیلم‌ها را ویرایش و حذف کنیم و تغییرات را بلافاصله مشاهده نماییم.

مدیریت خطاها و وضعیت Loading

در زمان ساخت اپلیکیشن، ما مدیریت خطاها را با نمایش یک متن ساده مانند “Error...” انجام دادیم.

در دنیای واقعی، بهتر است UI مناسبی طراحی کنیم که خطاها را به صورت دقیق نمایش دهد و مشخص کند چه مشکلی پیش آمده است.

به همین ترتیب، هنگام بارگذاری داده‌ها، بهتر است به جای یک متن ساده، از یک Loading Spinner یا Skeleton UI استفاده کنیم تا کاربران متوجه شوند که داده‌ها در حال دریافت هستند.

در این مقاله، به جزئیات مدیریت خطاهای پیشرفته یا مدیریت پیشرفته وضعیت بارگذاری نمی‌پردازیم، اما این‌ها مواردی هستند که باید در اپلیکیشن‌های واقعی به آن‌ها توجه داشته باشیم.

بررسی بهترین روش‌ها هنگام کار با RTK Query

در ادامه، چند مورد از بهترین روش‌ها هنگام کار با RTK Query را باهم بررسی می‌کنیم که عبارتند از:

  1. جدا کردن APIهای مختلف: اگر برای APIهای مختلف از چندین API Slice استفاده می‌کنیم، بهتر است آن‌ها را در فایل‌های جداگانه نگه داریم. این کار باعث ماژولار شدن کد ما می‌شود و نگه‌داری و دیباگ کردن آن را آسان‌تر می‌کند.
  2. استفاده از Redux DevTools : Redux DevTools به ما اجازه می‌دهد که وضعیت Redux store، درخواست‌های query و mutation را مشاهده کنیم. این ابزار که به صورت افزونه مرورگر کروم موجود است، فرآیند دیباگ را ساده‌تر می‌کند.
  3. prefetch کردن دادها: بهتر است از هوک usePrefetch برای دریافت داده‌ها قبل از اینکه کاربر به صفحه‌ای خاص برود، استفاده کنیم. این کار باعث کاهش زمان بارگذاری صفحه و بهبود تجربه کاربری می‌شود.
  4. استفاده از Middleware برای منطق پیچیده: اگر لازم باشد درخواست‌ها را قبل از ارسال اصلاح کنیم (مثلاً توکن احراز هویت را به هدر اضافه کنیم یا خطاهای خاص را لاگ کنیم)، می‌توانیم از Middleware استفاده کنیم.
  5. استفاده از Optimistic Updateها: هنگام استفاده از useMutation برای به‌روزرسانی داده‌ها، می‌توانیم از Optimistic Update استفاده کنیم تا تغییرات در UI بلافاصله اعمال شوند. اگر درخواست با خطا مواجه شد، می‌توانیم تغییرات را به حالت قبل برگردانیم.

جمع‌بندی

در این مقاله، یاد گرفتیم که RTK Query چیست و چگونه می‌توانیم آن را با Redux Toolkit ادغام کنیم. همچنین یک اپلیکیشن CRUD برای مدیریت فیلم‌ها با React ساختیم. علاوه بر این، مفاهیم caching و invalidate کردن کش در RTK Query را نیز باهم بررسی کردیم.