Redux Toolkit چیست؟ آموزش کامل مدیریت State در React با ساخت اپلیکیشن هوش مصنوعی
در این آموزش Redux Toolkit را از صفر یاد میگیرید؛ از Store، Slice، Action، Reducer، useSelector و useDispatch تا عملیات Async، RTK Query و TypeScript. در پایان یک اپلیکیشن هوش مصنوعی React با مدیریت مدل، تنظیمات و مکالمات میسازیم.
با بزرگ شدن اپلیکیشن React، مدیریت State به یکی از مهمترین مسائل معماری تبدیل میشود. در یک ابزار ساده شاید چند useState کافی باشد، اما یک داشبورد هوش مصنوعی واقعی معمولاً باید مدل انتخابشده، تنظیمات تولید، پیامها، وضعیت کاربر، Theme، فیلترها، تاریخچه مکالمه و وضعیت درخواستهای API را میان چندین Component به اشتراک بگذارد.
اگر این اطلاعات بدون ساختار مشخص میان Componentها منتقل شوند، با مشکلاتی مانند Prop Drilling، State تکراری، داده ناسازگار، درخواستهای شبکه تکراری و منطق پراکنده روبهرو میشویم.
Redux Toolkit ابزار استاندارد اکوسیستم Redux برای مدیریت این نوع State است. این کتابخانه تنظیم Store، ساخت Reducer، تولید Action، مدیریت عملیات Async و دریافت داده از API را نسبت به Redux قدیمی بسیار سادهتر میکند.
اما Redux Toolkit چیست؟ چه تفاوتی با Redux، Context API و useState دارد؟ Store، Action، Reducer و Slice چه هستند؟ چگونه از createAsyncThunk یا RTK Query استفاده کنیم؟ چه دادهای باید داخل Redux قرار بگیرد و چه دادهای نباید در Store ذخیره شود؟
در این راهنما، Redux Toolkit را از مفاهیم پایه تا ساخت یک اپلیکیشن هوش مصنوعی واقعی با React بررسی میکنیم.
Redux Toolkit چیست؟
Redux Toolkit که معمولاً با نام کوتاه RTK شناخته میشود، مجموعه ابزار رسمی و استاندارد برای نوشتن منطق Redux است.
Redux Toolkit برای ساده کردن مشکلات رایج Redux قدیمی طراحی شده است:
- تنظیم پیچیده Store
- نیاز به Packageهای متعدد
- Boilerplate زیاد
- نوشتن دستی Action Type
- نوشتن Action Creatorهای تکراری
- پیادهسازی دشوار Updateهای Immutable
- مدیریت پراکنده عملیات Async
- دریافت و Cache کردن دادههای API
براساس مستندات رسمی Redux Toolkit، این Package بهعنوان روش استاندارد نوشتن منطق Redux طراحی شده و ابزارهایی مانند configureStore، createSlice، createAsyncThunk و RTK Query را ارائه میکند.
Redux چیست؟
Redux یک کتابخانه مدیریت State است که State مشترک برنامه را در یک Store مرکزی نگه میدارد.
چرخه ساده Redux:
رابط کاربری
|
| dispatch(action)
v
Redux Store
|
| اجرای reducer
v
State جدید
|
| selector
v
بهروزرسانی رابط کاربری
برای مثال کاربر مدل هوش مصنوعی جدیدی را انتخاب میکند:
کلیک روی مدل
|
v
dispatch(setSelectedModel(modelId))
|
v
Reducer اجرا میشود
|
v
selectedModelId تغییر میکند
|
v
Componentهای وابسته دوباره Render میشوند
تفاوت Redux و Redux Toolkit
Redux هسته اصلی مدیریت State و Redux Toolkit مجموعه ابزار پیشنهادی برای استفاده عملی از Redux است.
| معیار | Redux قدیمی | Redux Toolkit |
|---|---|---|
| تنظیم Store | دستیتر | ساده با configureStore |
| تعریف Action | معمولاً دستی | خودکار با createSlice |
| Action Type | رشتههای دستی | تولید خودکار |
| Update غیرقابلتغییر | نیازمند کپی دستی | ساده با Immer |
| Async Logic | تنظیم دستی Thunk | Thunk بهصورت پیشفرض |
| Data Fetching | نیازمند منطق جداگانه | RTK Query |
| Boilerplate | زیاد | کمتر |
| Redux DevTools | تنظیم جداگانه | پشتیبانی پیشفرض |
| روش پیشنهادی امروز | برای یادگیری مفاهیم | برای پروژههای واقعی |
اگر پروژه جدیدی میسازید، معمولاً Redux Toolkit انتخاب مناسبتری از نوشتن Redux به روش قدیمی است.
آیا Redux Toolkit جایگزین Redux است؟
Redux Toolkit از Redux استفاده میکند و جایگزین مفاهیم اصلی آن نیست. شما همچنان با این مفاهیم کار میکنید:
- Store
- State
- Action
- Reducer
- Dispatch
- Selector
Redux Toolkit فقط پیادهسازی آنها را سادهتر و استانداردتر میکند.
Redux چه مشکلی را حل میکند؟
در React، داده معمولاً از Component والد به فرزند از طریق Props منتقل میشود:
<App
selectedModel={selectedModel}
>
<Dashboard
selectedModel={selectedModel}
>
<Playground
selectedModel={selectedModel}
/>
</Dashboard>
</App>
اگر Componentهای زیادی به یک State نیاز داشته باشند، مجبور میشویم آن را از چند سطح عبور دهیم. به این وضعیت Prop Drilling گفته میشود.
Redux یک Store مرکزی ایجاد میکند:
Redux Store
├── user
├── settings
├── chat
├── models
└── ui
هر Component میتواند فقط بخش موردنیاز خود را بخواند:
const selectedModelId = useSelector(
(state) => state.settings.selectedModelId
);
و در صورت نیاز Action ارسال کند:
dispatch(
setSelectedModelId("YOUR_MODEL_ID")
);
آیا هر پروژه React به Redux نیاز دارد؟
خیر. Redux برای همه پروژهها ضروری نیست.
اگر پروژه کوچک است و State بیشتر در یک یا دو Component استفاده میشود، useState یا useReducer ممکن است کافی باشد.
Redux زمانی مفیدتر است که:
- State میان Componentهای دور از هم مشترک است.
- چند بخش برنامه یک State را تغییر میدهند.
- منطق تغییر State پیچیده است.
- نیاز به Debug دقیق تغییرات دارید.
- State باید ساختار قابلپیشبینی داشته باشد.
- پروژه تیمی و در حال رشد است.
- دادههای API به Cache و همگامسازی نیاز دارند.
- تاریخچه تغییر Actionها برای Debug مهم است.
- میخواهید منطق State از رابط کاربری جدا باشد.
چه زمانی Redux انتخاب مناسبی نیست؟
در این سناریوها Redux ممکن است پیچیدگی غیرضروری ایجاد کند:
- فرم ساده با چند Input
- Modal محلی یک Component
- وضعیت Hover
- باز یا بسته بودن یک Menu کوچک
- پروژه آموزشی بسیار کوچک
- State فقط در یک Component استفاده میشود
- تیم بدون نیاز واقعی فقط بهدلیل محبوبیت Redux آن را انتخاب کرده است
استفاده از Redux باید یک تصمیم معماری باشد، نه یک الزام برای تمام پروژههای React.
مقایسه useState، Context API و Redux Toolkit
| معیار | useState | Context API | Redux Toolkit |
|---|---|---|---|
| State محلی | عالی | مناسب نیست | معمولاً لازم نیست |
| State مشترک ساده | محدود | مناسب | مناسب |
| منطق پیچیده | دشوارتر | ممکن است شلوغ شود | مناسب |
| Debug تغییرات | محدود | محدود | Redux DevTools |
| Middleware | ندارد | ندارد | دارد |
| Data Fetching و Cache | دستی | دستی | RTK Query |
| ساختار استاندارد | ساده | وابسته به تیم | قوی |
| Boilerplate | کم | متوسط | متوسط |
| مقیاس پروژه بزرگ | محدود | وابسته به طراحی | مناسب |
یک الگوی منطقی:
useStateبرای State محلی- Context برای مقدارهای مشترک کمتغییر مانند Theme
- Redux Toolkit برای State مشترک پیچیده
- RTK Query برای Server State و Cache دادههای API
این مرزها قطعی نیستند و به معماری پروژه بستگی دارند.
مفاهیم اصلی Redux
پیش از نوشتن کد، باید چند مفهوم اصلی را بشناسید.
State چیست؟
State اطلاعات فعلی برنامه است:
{
settings: {
selectedModelId: "YOUR_MODEL_ID",
temperature: 0.4,
maxTokens: 1000
},
chat: {
messages: [],
status: "idle",
error: null
}
}
Redux State باید تا حد امکان قابل پیشبینی و Serializable باشد.
Store چیست؟
Store محل مرکزی نگهداری Redux State است:
const store = configureStore({
reducer: {
settings: settingsReducer,
chat: chatReducer,
},
});
Store علاوه بر State، متدهای Dispatch و Subscribe را نیز فراهم میکند.
در بیشتر برنامههای Client-side React فقط یک Redux Store اصلی داریم.
Action چیست؟
Action یک Object ساده است که رخ دادن یک رویداد را توضیح میدهد:
{
type: "settings/modelChanged",
payload: "YOUR_MODEL_ID"
}
در Redux Toolkit معمولاً Action Creator بهصورت خودکار ساخته میشود:
setSelectedModelId(
"YOUR_MODEL_ID"
);
خروجی این تابع یک Action است.
Reducer چیست؟
Reducer مشخص میکند State در پاسخ به یک Action چگونه تغییر کند.
function reducer(state, action) {
if (
action.type ===
"settings/modelChanged"
) {
return {
...state,
selectedModelId:
action.payload,
};
}
return state;
}
در Redux Toolkit معمولاً Reducerها داخل createSlice تعریف میشوند.
Dispatch چیست؟
Dispatch برای ارسال Action به Store استفاده میشود:
dispatch(
setSelectedModelId(
"YOUR_MODEL_ID"
)
);
پس از Dispatch:
- Action به Store ارسال میشود.
- Reducer مربوط اجرا میشود.
- State جدید تولید میشود.
- Componentهای وابسته به State بهروزرسانی میشوند.
Selector چیست؟
Selector تابعی برای خواندن بخشی از State است:
const selectSelectedModelId =
(state) =>
state.settings.selectedModelId;
در Component:
const selectedModelId =
useSelector(
selectSelectedModelId
);
Selector کمک میکند ساختار داخلی Store مستقیماً در تمام Componentها تکرار نشود.
Slice چیست؟
Slice بخشی از Redux State بههمراه Reducerها و Actionهای مربوط به آن است.
برای مثال:
settingsSlice
├── initialState
├── reducers
├── actions
└── reducer
نمونه:
import {
createSlice,
} from "@reduxjs/toolkit";
const settingsSlice =
createSlice({
name: "settings",
initialState: {
selectedModelId:
"YOUR_MODEL_ID",
temperature: 0.4,
},
reducers: {
setSelectedModelId(
state,
action
) {
state.selectedModelId =
action.payload;
},
setTemperature(
state,
action
) {
state.temperature =
action.payload;
},
},
});
Redux Toolkit بهصورت خودکار Action Creatorها و Reducer را تولید میکند.
نصب Redux Toolkit در React
برای یک پروژه موجود:
npm install @reduxjs/toolkit react-redux
Packageها:
@reduxjs/toolkit: ابزارهای Redux Toolkitreact-redux: اتصال Redux Store به React
این دستور مطابق راهنمای Quick Start رسمی Redux Toolkit است.
ساخت پروژه با Vite
npm create vite@latest redux-ai-app -- --template react
cd redux-ai-app
npm install
npm install @reduxjs/toolkit react-redux
npm run dev
ساختار پیشنهادی:
src/
├── app/
│ └── store.js
├── features/
│ ├── settings/
│ │ ├── settingsSlice.js
│ │ └── ModelSettings.jsx
│ ├── chat/
│ │ ├── chatSlice.js
│ │ └── ChatPanel.jsx
│ └── models/
│ └── modelsApi.js
├── App.jsx
└── main.jsx
ساخت اولین Store
فایل src/app/store.js:
import {
configureStore,
} from "@reduxjs/toolkit";
export const store =
configureStore({
reducer: {},
});
configureStore بسیاری از تنظیمات استاندارد Redux را بهصورت پیشفرض انجام میدهد:
- ترکیب Reducerها
- اضافه کردن Middlewareهای پیشفرض
- پشتیبانی از Redux Thunk
- بررسی بعضی اشتباهات متداول در محیط توسعه
- اتصال به Redux DevTools
طبق مستندات Redux Toolkit، configureStore تنظیم Store را ساده میکند و پیشفرضهای مناسبی در اختیار برنامه قرار میدهد.
اتصال Store به React با Provider
فایل src/main.jsx:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { store } from "./app/store";
import App from "./App";
import "./index.css";
const rootElement =
document.getElementById("root");
if (!rootElement) {
throw new Error(
"Root element was not found."
);
}
createRoot(rootElement).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>
);
Provider، Store را از طریق React Context در دسترس Componentهای فرزند قرار میدهد.
اگر useSelector یا useDispatch خارج Provider اجرا شود، خطا دریافت خواهید کرد.
ساخت settingsSlice
فایل src/features/settings/settingsSlice.js:
import {
createSlice,
} from "@reduxjs/toolkit";
const initialState = {
selectedModelId:
"YOUR_MODEL_ID",
temperature: 0.4,
maxTokens: 1000,
systemPrompt:
"شما یک دستیار فارسی دقیق و کاربردی هستید.",
};
const settingsSlice =
createSlice({
name: "settings",
initialState,
reducers: {
setSelectedModelId(
state,
action
) {
state.selectedModelId =
action.payload;
},
setTemperature(
state,
action
) {
const value =
Number(action.payload);
if (
Number.isFinite(value) &&
value >= 0 &&
value <= 2
) {
state.temperature =
value;
}
},
setMaxTokens(
state,
action
) {
const value =
Number(action.payload);
if (
Number.isInteger(value) &&
value > 0
) {
state.maxTokens =
value;
}
},
setSystemPrompt(
state,
action
) {
state.systemPrompt =
String(action.payload);
},
resetSettings() {
return initialState;
},
},
});
export const {
setSelectedModelId,
setTemperature,
setMaxTokens,
setSystemPrompt,
resetSettings,
} = settingsSlice.actions;
export const selectSettings =
(state) =>
state.settings;
export const selectSelectedModelId =
(state) =>
state.settings
.selectedModelId;
export default settingsSlice.reducer;
چرا Reducer شبیه Mutation نوشته شده است؟
در Reducer بالا این کد را داریم:
state.temperature = value;
در Redux معمولی نباید State را مستقیماً تغییر داد. Redux Toolkit از Immer استفاده میکند و این کد را روی Draft State اجرا میکند. Immer براساس تغییرات، State جدید Immutable تولید میکند.
یعنی این کد:
state.temperature = value;
از دید Redux Toolkit یک Update غیرقابلتغییر معتبر ایجاد میکند.
اما این قابلیت فقط در Reducerهای پشتیبانیشده Redux Toolkit اعمال میشود. خارج از Reducer نباید Objectهای State را مستقیم تغییر دهید.
createSlice و استفاده داخلی آن از Immer در مستندات Quick Start توضیح داده شده است.
اضافه کردن Slice به Store
فایل src/app/store.js:
import {
configureStore,
} from "@reduxjs/toolkit";
import settingsReducer from
"../features/settings/settingsSlice";
export const store =
configureStore({
reducer: {
settings:
settingsReducer,
},
});
ساختار State:
{
settings: {
selectedModelId:
"YOUR_MODEL_ID",
temperature: 0.4,
maxTokens: 1000,
systemPrompt: "..."
}
}
نام settings در Store همان مسیری است که Selector استفاده میکند:
state.settings.temperature
خواندن State با useSelector
import {
useSelector,
} from "react-redux";
import {
selectSettings,
} from "./settingsSlice";
export default function ModelSettings() {
const settings =
useSelector(
selectSettings
);
return (
<pre>
{JSON.stringify(
settings,
null,
2
)}
</pre>
);
}
برای کاهش Renderهای غیرضروری بهتر است فقط مقدار موردنیاز را انتخاب کنید:
const temperature =
useSelector(
(state) =>
state.settings.temperature
);
بهجای انتخاب Object بزرگ:
const settings =
useSelector(
(state) =>
state.settings
);
اگر هر تغییر کوچک Object جدیدی ایجاد کند، Component انتخابکننده آن Object ممکن است دوباره Render شود.
تغییر State با useDispatch
import {
useDispatch,
useSelector,
} from "react-redux";
import {
selectSelectedModelId,
setSelectedModelId,
} from "./settingsSlice";
export default function ModelSelector() {
const dispatch =
useDispatch();
const selectedModelId =
useSelector(
selectSelectedModelId
);
return (
<label>
شناسه مدل
<input
type="text"
value={selectedModelId}
onChange={(event) => {
dispatch(
setSelectedModelId(
event.target.value
)
);
}}
/>
</label>
);
}
setSelectedModelId یک Action Creator است که توسط createSlice تولید شده است.
Payload در Action
وقتی این کد اجرا میشود:
dispatch(
setSelectedModelId(
"YOUR_MODEL_ID"
)
);
Action تقریباً چنین ساختاری دارد:
{
type:
"settings/setSelectedModelId",
payload:
"YOUR_MODEL_ID"
}
مقدار ورودی Action Creator در action.payload قرار میگیرد.
ساخت Reducer با چند مقدار Payload
addMessage: {
reducer(state, action) {
state.messages.push(
action.payload
);
},
prepare(role, content) {
return {
payload: {
id: crypto.randomUUID(),
role,
content,
createdAt:
new Date()
.toISOString(),
},
};
},
},
استفاده:
dispatch(
addMessage(
"user",
"سلام"
)
);
prepare به ساخت Payload استاندارد کمک میکند.
ساخت chatSlice
فایل src/features/chat/chatSlice.js:
import {
createSlice,
} from "@reduxjs/toolkit";
const initialState = {
activeConversationId: null,
messages: [],
status: "idle",
error: null,
};
const chatSlice =
createSlice({
name: "chat",
initialState,
reducers: {
startConversation(
state,
action
) {
state.activeConversationId =
action.payload;
state.messages = [];
state.error = null;
state.status = "idle";
},
addMessage: {
reducer(
state,
action
) {
state.messages.push(
action.payload
);
},
prepare(role, content) {
return {
payload: {
id:
crypto.randomUUID(),
role,
content,
createdAt:
new Date()
.toISOString(),
},
};
},
},
setChatStatus(
state,
action
) {
state.status =
action.payload;
},
setChatError(
state,
action
) {
state.error =
action.payload;
},
clearChat(state) {
state.messages = [];
state.error = null;
state.status = "idle";
},
},
});
export const {
startConversation,
addMessage,
setChatStatus,
setChatError,
clearChat,
} = chatSlice.actions;
export const selectMessages =
(state) =>
state.chat.messages;
export const selectChatStatus =
(state) =>
state.chat.status;
export const selectChatError =
(state) =>
state.chat.error;
export default chatSlice.reducer;
اضافه کردن به Store:
import chatReducer from
"../features/chat/chatSlice";
export const store =
configureStore({
reducer: {
settings:
settingsReducer,
chat:
chatReducer,
},
});
دادههای مناسب Redux Store
Redux برای دادههای مشترک و قابلپیشبینی مناسب است:
- تنظیمات مدل
- شناسه مدل انتخابی
- وضعیت احراز هویت کنترلشده
- اطلاعات عمومی کاربر
- فیلترهای مشترک
- وضعیت Sidebar
- تنظیم Theme
- Conversation انتخابشده
- پیامهای مدیریتشده در سطح برنامه
- Entityهای مشترک
- Cache دادههای API از طریق RTK Query
چه دادههایی را در Redux Store قرار ندهیم؟
از ذخیره این موارد در Store خودداری کنید مگر طراحی خاصی داشته باشید:
- API Key
- Password
- Refresh Token ناامن
- Function
- Promise
- DOM Element
AbortController- File Objectهای بزرگ بدون نیاز
- Connectionهای شبکه
- Error Object پیچیده
- Instance کلاس
- داده موقتی Hover
- State محلی ساده Input
- داده تکراری قابل محاسبه
- Objectهای غیرSerializable
نمونه نامناسب:
{
controller:
new AbortController(),
onComplete:
() => {},
error:
new Error("Failed")
}
نمونه مناسبتر:
{
requestId:
"request-123",
status:
"failed",
error: {
code:
"REQUEST_FAILED",
message:
"درخواست انجام نشد."
}
}
چرا Serializable بودن مهم است؟
Redux DevTools، Middlewareها، Persist کردن State و Replay Actionها با دادههای Serializable بهتر کار میکنند.
دادههای ساده مناسب:
- String
- Number
- Boolean
null- Array
- Object ساده
برای تاریخ بهجای Date Object میتوانید ISO String ذخیره کنید:
{
createdAt:
new Date()
.toISOString()
}
و هنگام نمایش:
const date =
new Date(message.createdAt);
Async Logic در Redux
Reducer باید Sync باشد و Side Effect اجرا نکند.
این کد داخل Reducer اشتباه است:
reducers: {
async sendMessage(
state,
action
) {
const response =
await fetch("/api/chat");
state.answer =
await response.json();
},
}
عملیات شبکه باید خارج Reducer انجام شود.
روشهای متداول:
- Thunk دستی
createAsyncThunk- RTK Query
- Middleware اختصاصی
برای دریافت و Cache کردن Server State، RTK Query اغلب انتخاب مناسبتری است. برای Workflowهای Async سفارشی، createAsyncThunk همچنان کاربردی است.
createAsyncThunk چیست؟
createAsyncThunk یک Action Async میسازد و براساس نتیجه Promise، Actionهای زیر را تولید میکند:
pending
fulfilled
rejected
براساس مستندات Redux Toolkit، این تابع یک Type Prefix و تابع Promise دریافت میکند و Actionهای Lifecycle را تولید میکند.
ساخت sendChatMessage با createAsyncThunk
فایل src/features/chat/chatThunks.js:
import {
createAsyncThunk,
} from "@reduxjs/toolkit";
export const sendChatMessage =
createAsyncThunk(
"chat/sendMessage",
async (
{
message,
},
{
getState,
rejectWithValue,
signal,
}
) => {
const state =
getState();
const {
selectedModelId,
temperature,
maxTokens,
systemPrompt,
} = state.settings;
try {
const response =
await fetch(
"/api/chat",
{
method: "POST",
headers: {
"Content-Type":
"application/json",
},
body:
JSON.stringify({
message,
modelId:
selectedModelId,
temperature,
maxTokens,
systemPrompt,
}),
signal,
}
);
const data =
await response
.json()
.catch(() => null);
if (!response.ok) {
return rejectWithValue({
code:
data?.code ||
"REQUEST_FAILED",
message:
data?.error ||
"دریافت پاسخ با خطا روبهرو شد.",
});
}
if (
typeof data?.answer !==
"string"
) {
return rejectWithValue({
code:
"INVALID_RESPONSE",
message:
"ساختار پاسخ قابل پردازش نیست.",
});
}
return {
answer:
data.answer,
};
} catch (error) {
if (
error.name ===
"AbortError"
) {
throw error;
}
return rejectWithValue({
code:
"NETWORK_ERROR",
message:
"ارتباط با سرور برقرار نشد.",
});
}
}
);
مدیریت Lifecycle در extraReducers
فایل chatSlice.js:
import {
createSlice,
} from "@reduxjs/toolkit";
import {
sendChatMessage,
} from "./chatThunks";
const initialState = {
messages: [],
status: "idle",
error: null,
};
const chatSlice =
createSlice({
name: "chat",
initialState,
reducers: {
addUserMessage: {
reducer(
state,
action
) {
state.messages.push(
action.payload
);
},
prepare(content) {
return {
payload: {
id:
crypto.randomUUID(),
role:
"user",
content,
createdAt:
new Date()
.toISOString(),
},
};
},
},
clearChat(state) {
state.messages = [];
state.status = "idle";
state.error = null;
},
},
extraReducers:
(builder) => {
builder
.addCase(
sendChatMessage.pending,
(state) => {
state.status =
"loading";
state.error =
null;
}
)
.addCase(
sendChatMessage.fulfilled,
(
state,
action
) => {
state.status =
"succeeded";
state.messages.push({
id:
crypto.randomUUID(),
role:
"assistant",
content:
action.payload
.answer,
createdAt:
new Date()
.toISOString(),
});
}
)
.addCase(
sendChatMessage.rejected,
(
state,
action
) => {
if (
action.meta.aborted
) {
state.status =
"idle";
return;
}
state.status =
"failed";
state.error =
action.payload || {
code:
"UNKNOWN_ERROR",
message:
action.error
.message ||
"خطای پیشبینینشدهای رخ داد.",
};
}
);
},
});
export const {
addUserMessage,
clearChat,
} = chatSlice.actions;
export default chatSlice.reducer;
ارسال Thunk در Component
import {
useRef,
useState,
} from "react";
import {
useDispatch,
useSelector,
} from "react-redux";
import {
addUserMessage,
} from "./chatSlice";
import {
sendChatMessage,
} from "./chatThunks";
export default function ChatForm() {
const dispatch =
useDispatch();
const [message, setMessage] =
useState("");
const activePromiseRef =
useRef(null);
const status =
useSelector(
(state) =>
state.chat.status
);
const error =
useSelector(
(state) =>
state.chat.error
);
async function handleSubmit(
event
) {
event.preventDefault();
const cleanMessage =
message.trim();
if (
!cleanMessage ||
status === "loading"
) {
return;
}
dispatch(
addUserMessage(
cleanMessage
)
);
setMessage("");
const promise =
dispatch(
sendChatMessage({
message:
cleanMessage,
})
);
activePromiseRef.current =
promise;
try {
await promise.unwrap();
} catch (requestError) {
if (
requestError?.name !==
"AbortError"
) {
console.error(
"Chat request failed."
);
}
} finally {
activePromiseRef.current =
null;
}
}
function handleCancel() {
activePromiseRef
.current
?.abort();
}
return (
<form
onSubmit={handleSubmit}
>
<label htmlFor="message">
پیام
</label>
<textarea
id="message"
value={message}
onChange={(event) =>
setMessage(
event.target.value
)
}
rows="7"
disabled={
status === "loading"
}
/>
<div>
{status ===
"loading" && (
<button
type="button"
onClick={
handleCancel
}
>
توقف
</button>
)}
<button
type="submit"
disabled={
status ===
"loading" ||
!message.trim()
}
>
ارسال
</button>
</div>
{error && (
<p role="alert">
{error.message}
</p>
)}
</form>
);
}
Promise برگشتی از Dispatch یک Async Thunk متد abort دارد. آن را داخل Redux State ذخیره نکردیم؛ زیرا Promise و کنترلکننده Request داده Serializable مناسبی برای Store نیستند.
unwrap چیست؟
Dispatch کردن Async Thunk همیشه یک Promise مربوط به Action نهایی برمیگرداند. با unwrap میتوان Payload موفق را دریافت یا خطای Rejectشده را Throw کرد:
try {
const result =
await dispatch(
sendChatMessage({
message: "سلام",
})
).unwrap();
console.log(result.answer);
} catch (error) {
console.error(error);
}
این روش برای زمانی مفید است که Component باید بعد از موفقیت یا شکست، عملیات دیگری انجام دهد.
RTK Query چیست؟
RTK Query ابزار Data Fetching و Cache موجود در Redux Toolkit است.
با RTK Query میتوان:
- Endpointها را تعریف کرد.
- Hookهای React تولید کرد.
- Loading و Error State دریافت کرد.
- دادهها را Cache کرد.
- Requestهای تکراری را کاهش داد.
- دادهها را Refetch کرد.
- Cache را براساس Tag باطل کرد.
- Polling انجام داد.
- داده را Transform کرد.
- Mutationهای API ساخت.
RTK Query داخل Package @reduxjs/toolkit قرار دارد و Package جداگانهای نیاز ندارد.
طبق مستندات RTK Query، این ابزار برای سادهسازی Data Fetching و Cache در برنامههای وب طراحی شده است.
تفاوت createAsyncThunk و RTK Query
| معیار | createAsyncThunk | RTK Query |
|---|---|---|
| Async Workflow سفارشی | عالی | محدودتر |
| دریافت داده API | دستی | آماده |
| Cache | دستی | داخلی |
| Deduplication درخواست | دستی | داخلی |
| Loading State | با Slice | Hook آماده |
| Error State | با Slice | Hook آماده |
| Refetch | دستی | داخلی |
| Invalidating Cache | دستی | Tag |
| منطق چندمرحلهای | مناسب | همیشه مناسب نیست |
| CRUD استاندارد | ممکن | بسیار مناسب |
راهنمای عمومی:
- برای Server State و CRUD از RTK Query استفاده کنید.
- برای Workflowهای Async پیچیده و Actionمحور از Thunk استفاده کنید.
- برای هر Request ساده یک Slice و Async Thunk جداگانه نسازید.
- RTK Query و Async Thunk را بدون دلیل برای یک داده واحد ترکیب نکنید.
ساخت RTK Query API Slice
فایل src/features/api/appApi.js:
import {
createApi,
fetchBaseQuery,
} from "@reduxjs/toolkit/query/react";
export const appApi =
createApi({
reducerPath:
"appApi",
baseQuery:
fetchBaseQuery({
baseUrl: "/api",
prepareHeaders(
headers
) {
headers.set(
"Accept",
"application/json"
);
return headers;
},
}),
tagTypes: [
"Models",
"Conversations",
],
endpoints:
(builder) => ({
getModels:
builder.query({
query:
(category) => ({
url: "/models",
params:
category &&
category !== "all"
? {
category,
}
: undefined,
}),
providesTags: [
"Models",
],
}),
getConversation:
builder.query({
query:
(conversationId) =>
`/conversations/${conversationId}`,
providesTags:
(
result,
error,
conversationId
) => [
{
type:
"Conversations",
id:
conversationId,
},
],
}),
sendChatMessage:
builder.mutation({
query:
(requestBody) => ({
url: "/chat",
method: "POST",
body: requestBody,
}),
}),
deleteConversation:
builder.mutation({
query:
(conversationId) => ({
url:
`/conversations/${conversationId}`,
method:
"DELETE",
}),
invalidatesTags:
(
result,
error,
conversationId
) => [
{
type:
"Conversations",
id:
conversationId,
},
{
type:
"Conversations",
id:
"LIST",
},
],
}),
}),
});
export const {
useGetModelsQuery,
useGetConversationQuery,
useSendChatMessageMutation,
useDeleteConversationMutation,
} = appApi;
اضافه کردن RTK Query به Store
import {
configureStore,
} from "@reduxjs/toolkit";
import {
setupListeners,
} from "@reduxjs/toolkit/query";
import settingsReducer from
"../features/settings/settingsSlice";
import chatReducer from
"../features/chat/chatSlice";
import {
appApi,
} from "../features/api/appApi";
export const store =
configureStore({
reducer: {
settings:
settingsReducer,
chat:
chatReducer,
[appApi.reducerPath]:
appApi.reducer,
},
middleware:
(getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(
appApi.middleware
),
});
setupListeners(
store.dispatch
);
اضافه کردن appApi.reducer برای نگهداری Cache و appApi.middleware برای مدیریت Requestها لازم است.
دریافت مدلها با RTK Query
import {
useSelector,
} from "react-redux";
import {
useGetModelsQuery,
} from "../api/appApi";
export default function ModelsList() {
const category =
useSelector(
(state) =>
state.settings
.modelCategory
);
const {
data: models = [],
isLoading,
isFetching,
isError,
error,
refetch,
} = useGetModelsQuery(
category
);
if (isLoading) {
return (
<p>
در حال دریافت مدلها...
</p>
);
}
if (isError) {
return (
<section>
<p role="alert">
دریافت مدلها
انجام نشد.
</p>
<button
type="button"
onClick={refetch}
>
تلاش دوباره
</button>
</section>
);
}
return (
<section>
{isFetching && (
<p>
در حال بهروزرسانی...
</p>
)}
<ul>
{models.map(
(model) => (
<li key={model.id}>
{model.name}
</li>
)
)}
</ul>
</section>
);
}
تفاوت isLoading و isFetching
در RTK Query:
isLoading: معمولاً برای اولین بار که دادهای وجود ندارد.isFetching: Request در حال انجام است، حتی اگر داده Cacheشده قبلی موجود باشد.
میتوانید هنگام Refetch داده قبلی را نگه دارید و فقط نشانگر کوچک بهروزرسانی نمایش دهید.
ارسال پیام با RTK Query Mutation
import {
useState,
} from "react";
import {
useDispatch,
useSelector,
} from "react-redux";
import {
useSendChatMessageMutation,
} from "../api/appApi";
import {
addMessage,
} from "../chat/chatSlice";
export default function ChatForm() {
const dispatch =
useDispatch();
const [message, setMessage] =
useState("");
const settings =
useSelector(
(state) =>
state.settings
);
const [
sendChatMessage,
{
isLoading,
error,
},
] =
useSendChatMessageMutation();
async function handleSubmit(
event
) {
event.preventDefault();
const cleanMessage =
message.trim();
if (
!cleanMessage ||
isLoading
) {
return;
}
dispatch(
addMessage(
"user",
cleanMessage
)
);
setMessage("");
try {
const result =
await sendChatMessage({
message:
cleanMessage,
modelId:
settings
.selectedModelId,
temperature:
settings.temperature,
maxTokens:
settings.maxTokens,
systemPrompt:
settings.systemPrompt,
}).unwrap();
dispatch(
addMessage(
"assistant",
result.answer
)
);
} catch (requestError) {
console.error(
"Chat request failed."
);
}
}
return (
<form
onSubmit={handleSubmit}
>
<textarea
value={message}
onChange={(event) =>
setMessage(
event.target.value
)
}
disabled={isLoading}
/>
<button
type="submit"
disabled={
isLoading ||
!message.trim()
}
>
{isLoading
? "در حال دریافت..."
: "ارسال"}
</button>
{error && (
<p role="alert">
درخواست با خطا
روبهرو شد.
</p>
)}
</form>
);
}
API Key در Redux Store قرار ندهید
این State ناامن است:
const initialState = {
apiKey:
"YOUR_DARVAREH_API_KEY",
};
Redux Store سمت Client است و کاربر میتواند State و Actionهای آن را با Developer Tools مشاهده کند.
این روش نیز امن نیست:
const apiKey =
import.meta.env
.VITE_DARVAREH_API_KEY;
API Key درواره باید فقط در Backend ذخیره شود:
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID
معماری مناسب:
React + Redux Toolkit
|
| /api/chat
v
Backend برنامه شما
|
| API Key خصوصی
v
API هوش مصنوعی درواره
ساخت Backend امن برای پروژه
در ریشه پروژه:
mkdir server
cd server
npm init -y
npm install express dotenv
فایل server/package.json:
{
"name": "redux-ai-backend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "node --watch server.js",
"start": "node server.js"
}
}
فایل server/.env:
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID
PORT=3000
فایل .env را وارد Git نکنید:
server/.env
node_modules
dist
برای شناسه مدل و قیمت بهروز، صفحه مدلهای درواره را بررسی کنید.
پیادهسازی Endpoint Chat
فایل server/server.js:
import "dotenv/config";
import express from "express";
const app = express();
const port =
Number(
process.env.PORT ||
3000
);
app.disable(
"x-powered-by"
);
app.use(
express.json({
limit: "32kb",
})
);
app.post(
"/api/chat",
async (
request,
response
) => {
const message =
typeof request.body
?.message ===
"string"
? request.body
.message
.trim()
: "";
const requestedModelId =
typeof request.body
?.modelId ===
"string"
? request.body
.modelId
.trim()
: "";
if (!message) {
return response
.status(400)
.json({
error:
"متن پیام الزامی است.",
});
}
if (
message.length >
4000
) {
return response
.status(400)
.json({
error:
"طول پیام بیشتر از مقدار مجاز است.",
});
}
if (
!process.env
.DARVAREH_API_KEY
) {
return response
.status(500)
.json({
error:
"تنظیمات سرویس کامل نیست.",
});
}
const modelId =
requestedModelId ||
process.env
.DARVAREH_MODEL_ID;
if (!modelId) {
return response
.status(400)
.json({
error:
"مدل انتخاب نشده است.",
});
}
const temperature =
Number(
request.body
?.temperature
);
const safeTemperature =
Number.isFinite(
temperature
) &&
temperature >= 0 &&
temperature <= 2
? temperature
: 0.4;
const maxTokens =
Number(
request.body
?.maxTokens
);
const safeMaxTokens =
Number.isInteger(
maxTokens
) &&
maxTokens > 0 &&
maxTokens <= 8000
? maxTokens
: 1000;
const systemPrompt =
typeof request.body
?.systemPrompt ===
"string"
? request.body
.systemPrompt
.trim()
.slice(0, 2000)
: "شما یک دستیار فارسی دقیق و کاربردی هستید.";
try {
const upstreamResponse =
await fetch(
"https://api.darvareh.ir/v1/chat/completions",
{
method:
"POST",
headers: {
Authorization:
`Bearer ${process.env.DARVAREH_API_KEY}`,
"Content-Type":
"application/json",
},
body:
JSON.stringify({
model:
modelId,
messages: [
{
role:
"system",
content:
systemPrompt,
},
{
role:
"user",
content:
message,
},
],
temperature:
safeTemperature,
max_tokens:
safeMaxTokens,
}),
}
);
const data =
await upstreamResponse
.json()
.catch(() => null);
if (
!upstreamResponse.ok
) {
console.error(
"Darvareh API error:",
{
status:
upstreamResponse
.status,
}
);
return response
.status(502)
.json({
error:
"سرویس هوش مصنوعی پاسخ معتبری برنگرداند.",
});
}
const answer =
data
?.choices?.[0]
?.message
?.content;
if (
typeof answer !==
"string" ||
!answer.trim()
) {
return response
.status(502)
.json({
error:
"ساختار پاسخ قابل پردازش نبود.",
});
}
return response.json({
answer:
answer.trim(),
});
} catch (error) {
console.error(
"Unexpected error:",
error
);
return response
.status(500)
.json({
error:
"ارتباط با سرویس برقرار نشد.",
});
}
}
);
app.listen(
port,
() => {
console.log(
`Backend is running on port ${port}`
);
}
);
Backend باید علاوه بر اعتبارسنجی ورودی، دسترسی کاربر به مدل، محدودیت مصرف، موجودی و سیاستهای محصول را نیز بررسی کند.
تنظیم Proxy در Vite
فایل vite.config.js:
import {
defineConfig,
} from "vite";
import react from
"@vitejs/plugin-react";
export default defineConfig({
plugins: [
react(),
],
server: {
proxy: {
"/api": {
target:
"http://localhost:3000",
changeOrigin:
true,
},
},
},
});
RTK Query اکنون Request را به /api/chat میفرستد و Vite آن را در محیط توسعه به Backend هدایت میکند.
ساخت Selectorهای قابل استفاده مجدد
بهجای تکرار مسیر State:
const messages =
useSelector(
(state) =>
state.chat.messages
);
Selector بسازید:
export const selectMessages =
(state) =>
state.chat.messages;
استفاده:
const messages =
useSelector(
selectMessages
);
مزایا:
- Component ساختار Store را کمتر میشناسد.
- Refactor سادهتر میشود.
- منطق انتخاب داده قابل تست است.
- Selectorهای ترکیبی قابل ساخت هستند.
Selector مشتقشده
بهجای ذخیره تعداد پیامها در State:
{
messages: [],
messageCount: 0
}
آن را محاسبه کنید:
export const selectMessageCount =
(state) =>
state.chat
.messages.length;
ذخیره داده قابل محاسبه میتواند باعث ناسازگاری شود:
messages.length = 5
messageCount = 4
createSelector و Memoization
برای محاسبه پیچیده از createSelector استفاده کنید:
import {
createSelector,
} from "@reduxjs/toolkit";
const selectMessages =
(state) =>
state.chat.messages;
const selectSearchQuery =
(state) =>
state.chat.searchQuery;
export const selectFilteredMessages =
createSelector(
[
selectMessages,
selectSearchQuery,
],
(
messages,
searchQuery
) => {
const query =
searchQuery
.trim()
.toLowerCase();
if (!query) {
return messages;
}
return messages.filter(
(message) =>
message.content
.toLowerCase()
.includes(query)
);
}
);
createSelector نتیجه را تا زمانی که Inputها تغییر نکردهاند Memoize میکند.
از Memoization برای محاسبات واقعاً مفید استفاده کنید. هر Selector ساده به createSelector نیاز ندارد.
Normalized State چیست؟
برای فهرست بزرگ Entityها، ذخیره Array تودرتو میتواند Update را دشوار کند.
ساختار Array:
{
conversations: [
{
id: "1",
title: "First"
},
{
id: "2",
title: "Second"
}
]
}
ساختار Normalized:
{
ids: [
"1",
"2"
],
entities: {
"1": {
id: "1",
title: "First"
},
"2": {
id: "2",
title: "Second"
}
}
}
Redux Toolkit ابزار createEntityAdapter را برای مدیریت این ساختار ارائه میکند.
استفاده از createEntityAdapter
import {
createEntityAdapter,
createSlice,
} from "@reduxjs/toolkit";
const conversationsAdapter =
createEntityAdapter({
sortComparer:
(a, b) =>
b.updatedAt
.localeCompare(
a.updatedAt
),
});
const conversationsSlice =
createSlice({
name:
"conversations",
initialState:
conversationsAdapter
.getInitialState({
status:
"idle",
}),
reducers: {
conversationAdded:
conversationsAdapter
.addOne,
conversationsReceived:
conversationsAdapter
.setAll,
conversationUpdated:
conversationsAdapter
.updateOne,
conversationRemoved:
conversationsAdapter
.removeOne,
},
});
Selectorهای آماده:
export const {
selectAll:
selectAllConversations,
selectById:
selectConversationById,
selectIds:
selectConversationIds,
} =
conversationsAdapter
.getSelectors(
(state) =>
state.conversations
);
برای فهرست کوچک، Adapter ممکن است ضروری نباشد؛ اما در مدیریت Entityهای متعدد مفید است.
State را براساس Feature تقسیم کنید
ساختار مناسب:
features/
├── auth/
│ └── authSlice.js
├── chat/
│ ├── chatSlice.js
│ └── ChatPanel.jsx
├── models/
│ ├── modelsApi.js
│ └── ModelsList.jsx
├── settings/
│ ├── settingsSlice.js
│ └── SettingsPanel.jsx
└── ui/
└── uiSlice.js
ساختار نامناسب:
redux/
├── actions/
├── reducers/
├── constants/
└── types/
ساختار قدیمی مبتنی بر نوع فایل ممکن است منطق یک Feature را در چند پوشه پراکنده کند. در پروژههای جدید معمولاً Colocation براساس Feature خواناتر است.
نامگذاری Actionها
نام Slice و Reducer نام Action را میسازند:
name: "settings"
setTemperature()
Action Type:
settings/setTemperature
از نامهای توصیفی استفاده کنید:
modelSelected
temperatureChanged
conversationCreated
chatCleared
نام Action بهتر است رویداد رخداده را توصیف کند.
Redux Middleware چیست؟
Middleware میان Dispatch Action و رسیدن آن به Reducer قرار میگیرد.
dispatch(action)
|
v
middleware
|
v
reducer
کاربردها:
- عملیات Async
- Logging
- Analytics
- مدیریت API
- تبدیل Action
- مشاهده و کنترل Workflow
Redux Toolkit Middlewareهای پیشفرض مفیدی دارد و RTK Query نیز Middleware اختصاصی خود را اضافه میکند.
ساخت Middleware ساده برای زمانسنجی
const timingMiddleware =
(storeApi) =>
(next) =>
(action) => {
const startedAt =
performance.now();
const result =
next(action);
const duration =
performance.now() -
startedAt;
if (
import.meta.env.DEV
) {
console.log(
"Redux action:",
{
type:
action.type,
duration,
}
);
}
return result;
};
اضافه کردن به Store:
export const store =
configureStore({
reducer: {
settings:
settingsReducer,
},
middleware:
(getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(
timingMiddleware
),
});
اطلاعات حساس Payload را در Log چاپ نکنید.
Redux DevTools
Redux Toolkit بهصورت پیشفرض امکان اتصال به Redux DevTools را در محیط توسعه فراهم میکند.
با DevTools میتوانید:
- State فعلی را ببینید.
- Actionهای Dispatchشده را بررسی کنید.
- Payload هر Action را ببینید.
- تغییر State را مقایسه کنید.
- ترتیب Actionها را بررسی کنید.
- بعضی Timelineها را Replay کنید.
- Cache و Requestهای RTK Query را ببینید.
از قرار دادن اطلاعات حساس در Store خودداری کنید؛ زیرا ابزارهای توسعه میتوانند آنها را نمایش دهند.
Persist کردن بخشی از State
ممکن است بخواهید تنظیمات غیرحساس مانند Theme یا مدل انتخابی را در localStorage ذخیره کنید.
خواندن State اولیه:
function loadSettings() {
try {
const rawValue =
localStorage.getItem(
"ai-settings"
);
if (!rawValue) {
return undefined;
}
const parsed =
JSON.parse(rawValue);
return {
selectedModelId:
typeof parsed
.selectedModelId ===
"string"
? parsed
.selectedModelId
: "YOUR_MODEL_ID",
temperature:
Number.isFinite(
parsed.temperature
)
? parsed.temperature
: 0.4,
maxTokens:
Number.isInteger(
parsed.maxTokens
)
? parsed.maxTokens
: 1000,
systemPrompt:
typeof parsed
.systemPrompt ===
"string"
? parsed
.systemPrompt
: "",
};
} catch {
return undefined;
}
}
در Store:
const savedSettings =
loadSettings();
export const store =
configureStore({
reducer: {
settings:
settingsReducer,
},
preloadedState:
savedSettings
? {
settings:
savedSettings,
}
: undefined,
});
ذخیره تغییرات:
store.subscribe(
() => {
const state =
store.getState();
localStorage.setItem(
"ai-settings",
JSON.stringify({
selectedModelId:
state.settings
.selectedModelId,
temperature:
state.settings
.temperature,
maxTokens:
state.settings
.maxTokens,
systemPrompt:
state.settings
.systemPrompt,
})
);
}
);
در پروژه واقعی بهتر است نوشتن در Storage را Debounce کنید.
در localStorage ذخیره نکنید:
- API Key
- Password
- Secret
- اطلاعات محرمانه
- Token حساس بلندمدت
- پیامهای خصوصی بدون سیاست مشخص
Redux Toolkit با TypeScript
Redux Toolkit پشتیبانی مناسبی از TypeScript دارد.
فایل src/app/store.ts:
import {
configureStore,
} from "@reduxjs/toolkit";
import settingsReducer from
"../features/settings/settingsSlice";
export const store =
configureStore({
reducer: {
settings:
settingsReducer,
},
});
export type RootState =
ReturnType<
typeof store.getState
>;
export type AppDispatch =
typeof store.dispatch;
ساخت Hookهای Typed
فایل src/app/hooks.ts:
import {
useDispatch,
useSelector,
} from "react-redux";
import type {
TypedUseSelectorHook,
} from "react-redux";
import type {
AppDispatch,
RootState,
} from "./store";
export const useAppDispatch =
useDispatch.withTypes<
AppDispatch
>();
export const useAppSelector =
useSelector.withTypes<
RootState
>();
اگر نسخه React Redux شما از withTypes پشتیبانی نکند، میتوانید از الگوی Typed قدیمیتر استفاده کنید:
export const useAppDispatch =
() =>
useDispatch<
AppDispatch
>();
export const useAppSelector:
TypedUseSelectorHook<
RootState
> =
useSelector;
نسخه Package و مستندات همان نسخه را بررسی کنید.
Slice با TypeScript
import {
createSlice,
} from "@reduxjs/toolkit";
import type {
PayloadAction,
} from "@reduxjs/toolkit";
interface SettingsState {
selectedModelId: string;
temperature: number;
maxTokens: number;
}
const initialState:
SettingsState = {
selectedModelId:
"YOUR_MODEL_ID",
temperature:
0.4,
maxTokens:
1000,
};
const settingsSlice =
createSlice({
name:
"settings",
initialState,
reducers: {
setSelectedModelId(
state,
action:
PayloadAction<string>
) {
state.selectedModelId =
action.payload;
},
setTemperature(
state,
action:
PayloadAction<number>
) {
state.temperature =
action.payload;
},
},
});
Async Thunk با TypeScript
import {
createAsyncThunk,
} from "@reduxjs/toolkit";
interface SendMessageInput {
message: string;
}
interface SendMessageResult {
answer: string;
}
interface ApiError {
code: string;
message: string;
}
export const sendMessage =
createAsyncThunk<
SendMessageResult,
SendMessageInput,
{
rejectValue:
ApiError;
}
>(
"chat/sendMessage",
async (
input,
{
rejectWithValue,
}
) => {
const response =
await fetch(
"/api/chat",
{
method:
"POST",
headers: {
"Content-Type":
"application/json",
},
body:
JSON.stringify(
input
),
}
);
const data =
await response
.json()
.catch(() => null);
if (!response.ok) {
return rejectWithValue({
code:
data?.code ||
"REQUEST_FAILED",
message:
data?.error ||
"درخواست انجام نشد.",
});
}
return {
answer:
data.answer,
};
}
);
بهینهسازی Render با useSelector
این Selector در هر Dispatch یک Object جدید میسازد:
const settings =
useSelector(
(state) => ({
modelId:
state.settings
.selectedModelId,
temperature:
state.settings
.temperature,
})
);
حتی اگر مقادیر تغییر نکرده باشند، Object جدید میتواند باعث Render شود.
راهکار اول، Selectorهای جدا:
const modelId =
useSelector(
(state) =>
state.settings
.selectedModelId
);
const temperature =
useSelector(
(state) =>
state.settings
.temperature
);
راهکار دوم، Selector Memoized:
const selectModelSettings =
createSelector(
[
(state) =>
state.settings
.selectedModelId,
(state) =>
state.settings
.temperature,
],
(
modelId,
temperature
) => ({
modelId,
temperature,
})
);
State را بیش از حد بزرگ نکنید
Store مرکزی نباید به محل ذخیره همه چیز تبدیل شود.
State محلی Input:
const [
message,
setMessage,
] = useState("");
معمولاً بهتر از Dispatch کردن Action برای هر کاراکتر است:
dispatch(
messageInputChanged(
event.target.value
)
);
مگر اینکه متن Input واقعاً باید میان چند صفحه مشترک، Persist یا از بیرون کنترل شود.
تست Reducer
Reducer یک تابع قابل تست است:
import settingsReducer, {
setTemperature,
} from "./settingsSlice";
test(
"temperature changes",
() => {
const initialState = {
selectedModelId:
"model-a",
temperature:
0.4,
maxTokens:
1000,
systemPrompt:
"",
};
const nextState =
settingsReducer(
initialState,
setTemperature(
0.8
)
);
expect(
nextState.temperature
).toBe(0.8);
expect(
initialState.temperature
).toBe(0.4);
}
);
تست بررسی میکند:
- State جدید درست است.
- State اولیه تغییر نکرده است.
تست Selector
import {
selectSelectedModelId,
} from "./settingsSlice";
test(
"selects model id",
() => {
const state = {
settings: {
selectedModelId:
"model-a",
},
};
expect(
selectSelectedModelId(
state
)
).toBe("model-a");
}
);
تست Component متصل به Redux
یک Store آزمایشی بسازید:
import {
configureStore,
} from "@reduxjs/toolkit";
import {
Provider,
} from "react-redux";
function renderWithStore(
ui,
{
preloadedState,
} = {}
) {
const store =
configureStore({
reducer: {
settings:
settingsReducer,
},
preloadedState,
});
return {
store,
...render(
<Provider store={store}>
{ui}
</Provider>
),
};
}
استفاده:
renderWithStore(
<ModelSettings />,
{
preloadedState: {
settings: {
selectedModelId:
"model-test",
temperature:
0.4,
maxTokens:
1000,
systemPrompt:
"",
},
},
}
);
اشتباهات رایج Redux Toolkit
فراموش کردن Provider
خطای متداول:
could not find react-redux context value
راهحل:
<Provider store={store}>
<App />
</Provider>
ثبت نکردن Reducer در Store
Slice ساخته شده اما Reducer اضافه نشده است:
reducer: {
settings:
settingsReducer,
}
تغییر مستقیم State خارج Reducer
اشتباه:
const settings =
store.getState()
.settings;
settings.temperature =
0.9;
درست:
store.dispatch(
setTemperature(
0.9
)
);
اجرای Request داخل Reducer
Reducer باید Pure و Sync باقی بماند. Request را با Thunk یا RTK Query اجرا کنید.
ذخیره داده غیرSerializable
Function، Promise، DOM Node، AbortController و Error پیچیده را داخل Store قرار ندهید.
ذخیره API Key در Store
Redux Store در Client قابل مشاهده است. Secret باید در Backend باشد.
استفاده از Redux برای همه Stateها
State محلی ساده را در Component نگه دارید.
انتخاب Object بزرگ با useSelector
فقط مقدارهای موردنیاز Component را انتخاب کنید.
ذخیره State مشتقشده
داده قابل محاسبه را با Selector بسازید، نه اینکه نسخه تکراری آن را ذخیره کنید.
درخواست API تکراری
برای Server State از RTK Query استفاده کنید تا Cache و Deduplication بهتر مدیریت شوند.
ساخت چند API Slice برای یک Base URL
راهنمای عمومی RTK Query این است که معمولاً برای هر Base URL یک API Slice داشته باشید و Endpointها را در همان ساختار توسعه دهید.
تغییر Middleware پیشفرض بهجای گسترش آن
اشتباه:
middleware: [
appApi.middleware,
]
ممکن است Middlewareهای پیشفرض را حذف کند.
مناسبتر:
middleware:
(getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(
appApi.middleware
)
خطاهای رایج و راهحلها
Cannot read properties of undefined
احتمالاً نام Reducer در Store با Selector یکسان نیست:
reducer: {
settings:
settingsReducer,
}
Selector باید بخواند:
state.settings
نه:
state.setting
Actions must be plain objects
ممکن است Store با روش نامناسب ساخته شده یا Middleware Async حذف شده باشد. از configureStore استفاده کنید.
A non-serializable value was detected
در Action یا State مقدار غیرSerializable قرار گرفته است.
خروجی Console معمولاً مسیر مشکل را نشان میدهد. بهجای غیرفعال کردن Check، داده را به ساختار ساده تبدیل کنید.
RTK Query Request اجرا نمیشود
بررسی کنید:
appApi.reducerبه Store اضافه شده است.appApi.middlewareاضافه شده است.- Hook داخل
Providerاجرا میشود. - Endpoint و Base URL درستاند.
- Backend فعال است.
- Proxy محیط توسعه تنظیم شده است.
Hook داده قبلی را نمایش میدهد
RTK Query داده را Cache میکند. Argument Query، Tagها و سیاست Refetch را بررسی کنید.
createAsyncThunk خطا را درست نشان نمیدهد
برای خطای کنترلشده از rejectWithValue استفاده کنید و در Reducer مقدار action.payload را بررسی کنید.
Dispatch نتیجه واقعی را Throw نمیکند
از unwrap استفاده کنید:
await dispatch(
sendMessage(input)
).unwrap();
Redux Toolkit در Next.js
استفاده از Redux در فریمورکهای Server Rendering نیازمند توجه بیشتری است:
- Store نباید ناخواسته میان Requestهای کاربران مشترک شود.
- Hydration باید درست انجام شود.
- Server State و Client State باید تفکیک شوند.
- همه دادهها نباید وارد Store شوند.
- Componentهای Server و Client نقش متفاوت دارند.
برای پروژه Next.js از راهنمای رسمی Redux Toolkit مربوط به Next.js و نسخه فریمورک خود استفاده کنید. Config یک SPA را مستقیماً در Server Rendering کپی نکنید.
Redux Toolkit در پروژه بزرگ
برای پروژه بزرگ:
- State را براساس Feature تقسیم کنید.
- Selectorهای عمومی بسازید.
- Server State را با RTK Query مدیریت کنید.
- Entityهای بزرگ را Normalize کنید.
- Actionها را براساس رویداد نامگذاری کنید.
- API Sliceها را بیدلیل تکثیر نکنید.
- State غیرSerializable ذخیره نکنید.
- تست Reducer و Selector بنویسید.
- Performance را با اندازهگیری بهینه کنید.
- از Redux DevTools برای Debug استفاده کنید.
- API Key و Secret را خارج Store نگه دارید.
چکلیست Production
پیش از انتشار بررسی کنید:
- Store با
configureStoreساخته شده است. - React داخل
Providerقرار دارد. - Sliceها براساس Feature سازماندهی شدهاند.
- Reducerها در Store ثبت شدهاند.
- Selectorهای قابل استفاده مجدد وجود دارند.
- State محلی بیدلیل وارد Redux نشده است.
- داده مشتقشده با Selector محاسبه میشود.
- Function، Promise و DOM Node داخل Store نیستند.
- API Key در Store یا Frontend وجود ندارد.
- Server State با روش مشخص مدیریت میشود.
- RTK Query Reducer و Middleware ثبت شدهاند.
- Error و Loading State نمایش داده میشوند.
- Request تکراری کنترل شده است.
- ورودی در Backend اعتبارسنجی میشود.
- دسترسی مدل در Backend بررسی میشود.
- هزینه و مصرف API پایش میشود.
- Reducerهای مهم تست شدهاند.
- Selectorهای پیچیده تست شدهاند.
- Redux DevTools در محیط توسعه بررسی شده است.
- Persist فقط برای داده غیرحساس استفاده میشود.
- Lint، Type Check، Test و Build اجرا میشوند.
پرسشهای متداول
Redux Toolkit چیست؟
Redux Toolkit مجموعه ابزار رسمی برای نوشتن منطق Redux است. این Package تنظیم Store، ساخت Slice، تولید Action، مدیریت Reducer، عملیات Async و Data Fetching را ساده میکند.
تفاوت Redux و Redux Toolkit چیست؟
Redux هسته مدیریت State است. Redux Toolkit روش استاندارد و سادهتر استفاده از Redux را فراهم میکند.
آیا برای Redux Toolkit باید Redux جداگانه نصب کنیم؟
Package @reduxjs/toolkit وابستگیهای لازم Redux را در اختیار پروژه قرار میدهد. برای اتصال به React نیز react-redux را نصب کنید:
npm install @reduxjs/toolkit react-redux
Store چیست؟
Store محل مرکزی نگهداری Redux State است.
Slice چیست؟
Slice بخشی از State بههمراه Reducerها، Actionها و منطق مربوط به همان Feature است.
createSlice چه کاری انجام میدهد؟
createSlice براساس نام Slice، State اولیه و Reducerها، Action Creator و Reducer نهایی را تولید میکند.
useSelector چیست؟
Hook مربوط به React Redux برای خواندن بخشی از Redux State است.
useDispatch چیست؟
Hook مربوط به ارسال Action به Redux Store است.
createAsyncThunk چیست؟
ابزاری برای ساخت عملیات Async است که Actionهای pending، fulfilled و rejected تولید میکند.
RTK Query چیست؟
ابزار Data Fetching و Cache موجود در Redux Toolkit است که Endpoint و Hookهای React تولید میکند.
RTK Query بهتر است یا createAsyncThunk؟
برای دریافت، Cache و همگامسازی Server State معمولاً RTK Query مناسبتر است. برای Workflowهای Async سفارشی و Actionمحور، createAsyncThunk میتواند انتخاب بهتری باشد.
Redux بهتر است یا Context API؟
برای State مشترک ساده، Context ممکن است کافی باشد. برای State پیچیده، Debug دقیق، Middleware و Data Fetching ساختاریافته، Redux Toolkit امکانات بیشتری دارد.
آیا Redux برای پروژه کوچک لازم است؟
خیر. useState، useReducer یا Context ممکن است کافی باشند.
آیا میتوان API Key را در Redux ذخیره کرد؟
خیر. Redux Store در Frontend قابل مشاهده است. API Key خصوصی باید در Backend و متغیر محیطی نگهداری شود.
آیا Redux State بعد از Refresh باقی میماند؟
بهصورت پیشفرض خیر. در صورت نیاز میتوان بخش غیرحساس State را در Storage ذخیره و هنگام شروع برنامه بازیابی کرد.
آیا باید Response همه APIها را در Redux ذخیره کنیم؟
خیر. برای Server State میتوانید از RTK Query استفاده کنید و برای داده محلی ساده از State Component بهره ببرید.
چگونه مدل هوش مصنوعی را انتخاب کنیم؟
شناسه مدل انتخابشده را میتوان در Slice تنظیمات نگهداری کرد؛ اما Backend باید معتبر بودن و مجاز بودن مدل را بررسی کند. فهرست مدلها و قیمتها در صفحه مدلهای درواره قرار دارد.
جمعبندی
Redux Toolkit روش استاندارد و مدرن استفاده از Redux در پروژههای React است. این ابزار Store مرکزی، Slice، Action، Reducer، Middleware، عملیات Async و Data Fetching را در یک ساختار منظم و قابل نگهداری ارائه میکند.
مهمترین نکات این آموزش:
- همه پروژههای React به Redux نیاز ندارند.
configureStoreبرای ساخت Store استفاده میشود.Provider، Store را در اختیار React قرار میدهد.createSlice، Reducer و Actionها را تولید میکند.useSelectorState را میخواند.useDispatchAction ارسال میکند.- Immer نوشتن Updateهای Immutable را ساده میکند.
- عملیات شبکه نباید داخل Reducer اجرا شود.
createAsyncThunkبرای Workflowهای Async مناسب است.- RTK Query دریافت و Cache داده API را ساده میکند.
- State غیرSerializable نباید داخل Store قرار بگیرد.
- State محلی ساده باید در Component باقی بماند.
- دادههای مشتقشده بهتر است با Selector محاسبه شوند.
- API Key هوش مصنوعی نباید در Redux یا Frontend ذخیره شود.
- Backend باید ورودی، دسترسی، مدل و محدودیت مصرف را بررسی کند.
برای ساخت داشبورد هوش مصنوعی، Playground، ابزار تولید محتوا یا اپلیکیشن سازمانی میتوانید Redux Toolkit را برای مدیریت State سمت Client و API هوش مصنوعی درواره را برای اتصال Backend به مدلهای مختلف استفاده کنید.
برای مشاهده شناسه مدلها و اطلاعات بهروز قیمت، صفحه مدلهای درواره را ببینید.
مقالات مرتبط
- React چیست؟ آموزش کامل ریاکت از صفر با پروژه عملی
- ساخت اپلیکیشن با هوش مصنوعی؛ راهنمای کامل و کاربردی
- ساخت چتبات هوش مصنوعی با Next.js، React و API درواره
- ساخت اپلیکیشن موبایل هوش مصنوعی با React Native و Expo
- آموزش Node.js و Express برای اتصال به API هوش مصنوعی
- آموزش اتصال API هوش مصنوعی به اپلیکیشن
- ساخت API هوش مصنوعی آماده Production
- ساخت تست نرمافزار و Unit Test با هوش مصنوعی
- دیباگ کد با هوش مصنوعی؛ آموزش تحلیل و رفع خطا
- Code Review با هوش مصنوعی و بررسی Pull Request
منابع تکمیلی
- راهنمای شروع Redux Toolkit
- Redux Toolkit Quick Start
- مستندات createSlice
- مستندات createAsyncThunk
- راهنمای RTK Query
- مستندات Redux
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.