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 تابع 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 را با ساخت یک اپلیکیشن ساده مدیریت فیلمها یاد میگیریم. در این اپلیکیشن، کاربران میتوانند فیلمهای ذخیره شده در بکاند را مشاهده کنند (البته این یک بکاند شبیهسازی شده خواهد بود)، فیلمهای جدید اضافه کنند و همچنین فیلمها را بهروزرسانی یا حذف نمایند. به طور کلی، یک اپلیکیشن 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 را روی پورت 8080 راهاندازی میکند.
در مرحلهی بعد، یک API Slice ایجاد میکنیم. این API Slice برای پیکربندی Redux store استفاده میشود.
به پوشهی 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 ایجاد شده است که یک آبجکت را به عنوان پارامتر دریافت میکند.
reducerPath مسیر مربوط به API Slice را مشخص میکند.baseQuery از fetchBaseQuery استفاده میکند. تابع fetchBaseQuery یک آبجکت را به عنوان پارامتر دریافت کرده و شامل ویژگی baseURL است که آدرس ریشهی API ما را مشخص میکند.http://localhost:8080 به عنوان آدرس سرور JSON استفاده کردهایم.endpoints، نحوهی تعامل API ما را مشخص میکند. این ویژگی یک تابع است که پارامتر builder را دریافت کرده و یک آبجکت شامل متدهایی مانند getMovies، addMovie، updateMovie و deleteMovie را return میکند، که برای تعامل با API مورد استفاده قرار میگیرد.endpoints نامگذاری میشوند.این هوکهای سفارشی به ما این امکان را میدهند که از داخل کامپوننتهای فانکشنال با API تعامل داشته باشیم.
در این مرحله، باید 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 راهاندازی کردهایم.
reducer یک reducer را برای بهروزرسانی state در Redux Store مشخص میکند. moviesApiSlice.reducer reducerای است که state مربوط به API ما را بهروزرسانی میکند.middleware، یک Middleware برای مدیریت بهروزرسانیهای غیرهمزمان state ایجاد کردهایم. نیازی نیست نگران این بخش باشیم، این ویژگی برای انجام عملیات Caching و سایر قابلیتهای RTK Query ضروری است.قبل از ادامهی کار، باید 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>
);
در کد بالا:
Provider از react-redux و store که قبلاً ایجاد کردیم، import شده است.Provider اطراف کامپوننت App قرار داده شده است.store برای ارسال Redux Store به کل برنامه استفاده میشود.در این بخش، قصد داریم تا کامپوننت 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 نمایش داده میشوند. هر MovieCard دادههای یک movie را دریافت کرده و تابع deleteMovie را برای حذف فیلم اجرا میکند.در پوشه 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 دریافت میکند:
movie: اطلاعات فیلمdeleteMovie: تابعی برای حذف فیلممدیریت نمایش Modal:
dialogRef برای مدیریت رفرنس Modal استفاده میکنیم.selectedMovie با مقدار prop movie مقداردهی اولیه میشود و فیلمی را که برای ویرایش انتخاب شده، ذخیره میکند.تابع handleSelectedMovie با کلیک روی دکمه Edit فراخوانی میشود و کارهای زیر را انجام میدهد:
selectedMovie را روی آبجکت فیلم فعلی تنظیم میکند.EditModal را با استفاده از dialogRef.current?.showModal() باز میکند.document.body.style.overflow روی 'hidden' از پیمایش صفحه در زمانی که Modal باز است، جلوگیری میکند.تابع closeDialog با استفاده از dialogRef.current?.close() Modal باز را می بندد و با تنظیم document.body.style.overflow به 'visible' رفتار اسکرول صفحه را مجددا فعال میکند.
در دستور return، یک ساختار JSX بازگردانده میشود که:
h3 قرار داد نشان میدهد،handleSelectedMovie را برای باز کردن EditModal فعال میکند.deleteMovie را فراخوانی میکند و فیلم مشخص شده را از API حذف مینماید.کامپوننت EditModal در اینجا نمایش داده شده و dialogRef، closeDialog و selectedMovie را دریافت میکند.
در پوشه 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;
کدی که داریم کارهای زیر را انجام میدهد:
<dialog> برای ایجاد Modal ویرایش فیلم استفاده میکند.form داخل المنت dialog، اطلاعات فیلم انتخاب شده را نمایش میدهد.useUpdateMovieMutation برای ارسال درخواست ویرایش به API مورد استفاده قرار میگیرد.handleUpdateMovie:
updateMovie بهروزرسانی میکند.closeDialog Modal را میبندد.به فایل 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 چگونه کار میکند و چطور میتوانیم کشها را 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 به طور خودکار دادهها را دوباره دریافت کند.
با این تغییرات، اکنون میتوانیم فیلمها را ویرایش و حذف کنیم و تغییرات را بلافاصله مشاهده نماییم.
در زمان ساخت اپلیکیشن، ما مدیریت خطاها را با نمایش یک متن ساده مانند “Error...” انجام دادیم.
در دنیای واقعی، بهتر است UI مناسبی طراحی کنیم که خطاها را به صورت دقیق نمایش دهد و مشخص کند چه مشکلی پیش آمده است.
به همین ترتیب، هنگام بارگذاری دادهها، بهتر است به جای یک متن ساده، از یک Loading Spinner یا Skeleton UI استفاده کنیم تا کاربران متوجه شوند که دادهها در حال دریافت هستند.
در این مقاله، به جزئیات مدیریت خطاهای پیشرفته یا مدیریت پیشرفته وضعیت بارگذاری نمیپردازیم، اما اینها مواردی هستند که باید در اپلیکیشنهای واقعی به آنها توجه داشته باشیم.
در ادامه، چند مورد از بهترین روشها هنگام کار با RTK Query را باهم بررسی میکنیم که عبارتند از:
usePrefetch برای دریافت دادهها قبل از اینکه کاربر به صفحهای خاص برود، استفاده کنیم. این کار باعث کاهش زمان بارگذاری صفحه و بهبود تجربه کاربری میشود.useMutation برای بهروزرسانی دادهها، میتوانیم از Optimistic Update استفاده کنیم تا تغییرات در UI بلافاصله اعمال شوند. اگر درخواست با خطا مواجه شد، میتوانیم تغییرات را به حالت قبل برگردانیم.در این مقاله، یاد گرفتیم که RTK Query چیست و چگونه میتوانیم آن را با Redux Toolkit ادغام کنیم. همچنین یک اپلیکیشن CRUD برای مدیریت فیلمها با React ساختیم. علاوه بر این، مفاهیم caching و invalidate کردن کش در RTK Query را نیز باهم بررسی کردیم.