React چیست؟ آموزش کامل ریاکت از صفر با ساخت اپلیکیشن هوش مصنوعی
در این آموزش React را از صفر تا سطح کاربردی یاد میگیرید؛ از Component، JSX، Props و State تا Hook، useEffect، فرم و ارتباط با API. در پایان یک اپلیکیشن هوش مصنوعی واقعی با React، Vite و API درواره میسازیم.
React یا ریاکت یکی از محبوبترین ابزارهای توسعه رابط کاربری وب است. React به توسعهدهنده اجازه میدهد رابطهای پیچیده را از Componentهای کوچک، مستقل و قابلاستفاده مجدد بسازد.
بهجای اینکه تمام صفحه را در یک فایل بزرگ مدیریت کنیم، میتوانیم بخشهایی مانند Header، فرم، کارت محصول، پیام گفتوگو و دکمه را به Componentهای جداگانه تبدیل کنیم. هر Component ورودی مشخصی دریافت میکند، State خود را مدیریت میکند و در صورت تغییر داده، بخش مرتبط رابط را دوباره نمایش میدهد.
در این آموزش ابتدا مفاهیم اصلی React را یاد میگیریم و سپس یک اپلیکیشن واقعی میسازیم: یک دستیار فارسی که با React و Vite در Frontend اجرا میشود، یک Backend کوچک Node.js دارد و از طریق API درواره به مدل هوش مصنوعی انتخابشده متصل میشود.
React چیست؟
React یک کتابخانه JavaScript برای ساخت رابط کاربری است. در React، رابط از Componentها تشکیل میشود و هر Component میتواند بخشی از UI را براساس داده و State نمایش دهد.
مثال یک Component ساده:
function Welcome() {
return <h1>به آموزش React خوش آمدید</h1>;
}
استفاده از Component:
function App() {
return (
<main>
<Welcome />
<p>اولین اپلیکیشن React شما آماده است.</p>
</main>
);
}
براساس مستندات رسمی React، مفاهیم پایه این کتابخانه شامل Component، JSX، نمایش داده، شرط، فهرست، Event، State و اشتراکگذاری داده میان Componentها است.
آیا React فریمورک است یا کتابخانه؟
React در مستندات رسمی بهعنوان کتابخانه رابط کاربری معرفی میشود. React بهتنهایی تمام نیازهای یک اپلیکیشن وب را پوشش نمیدهد.
برای امکانات دیگری مانند موارد زیر معمولاً ابزارهای تکمیلی انتخاب میشوند:
- Routing
- دریافت و Cache کردن داده
- ساخت و Bundle کردن پروژه
- Server-side Rendering
- مدیریت فرمهای پیچیده
- تست
- Authentication
- مدیریت State سراسری
- Backend
فریمورکهایی مانند Next.js از React استفاده میکنند و قابلیتهای بیشتری مانند Routing، Rendering سمت سرور و ساختار پروژه ارائه میدهند.
در این آموزش برای یادگیری خود React از Vite استفاده میکنیم تا لایههای اصلی React را بدون پیچیدگی یک فریمورک Full Stack ببینیم.
React چه مشکلی را حل میکند؟
فرض کنید یک رابط گفتوگوی هوش مصنوعی دارید. این صفحه شامل بخشهای زیر است:
- Header
- انتخاب مدل
- فهرست پیامها
- پیام کاربر
- پیام دستیار
- فرم ارسال
- دکمه توقف
- نمایش خطا
- وضعیت Loading
- شمارنده پیامها
در JavaScript خالص میتوان تمام این بخشها را ساخت، اما با افزایش State و تعاملها، هماهنگ نگهداشتن DOM با داده دشوارتر میشود.
در React، شما مشخص میکنید رابط در هر State باید چگونه به نظر برسد. وقتی State تغییر کند، React بخش لازم از رابط را بهروزرسانی میکند.
نمایش مفهومی:
State برنامه
↓
Componentهای React
↓
رابط کاربری
برای مثال:
function Status({ isLoading }) {
return (
<span>
{isLoading ? "در حال دریافت پاسخ" : "آماده"}
</span>
);
}
با تغییر isLoading، متن مناسب نمایش داده میشود.
تفاوت React با JavaScript خالص
React جای JavaScript را نمیگیرد. React با JavaScript ساخته شده و کد React در نهایت به JavaScript قابلاجرا تبدیل میشود.
| موضوع | JavaScript خالص | React |
|---|---|---|
| ساخت رابط | تغییر مستقیم DOM | تعریف UI براساس State |
| استفاده مجدد | تابع و Module | Component |
| مدیریت State | دستی | Hookها و الگوهای React |
| ساختار پروژه بزرگ | نیازمند معماری دستی | معماری Component-based |
| اکوسیستم | Web APIهای مرورگر | اکوسیستم React |
| نیاز به JavaScript | پایه کار | ضروری |
قبل از یادگیری React بهتر است این مفاهیم JavaScript را بشناسید:
constوlet- تابع
- Arrow Function
- Array و Object
mapوfilter- Destructuring
- Spread Syntax
- Module
- Promise
- Async/Await
- Fetch API
- Event
تفاوت React و React Native
React برای ساخت رابط وب استفاده میشود و معمولاً خروجی آن عناصر HTML است.
React Native برای ساخت اپلیکیشن موبایل استفاده میشود و Componentهای بومی موبایل را نمایش میدهد.
// React وب
<button>ارسال</button>
// React Native
<Button title="ارسال" />
مفاهیمی مانند Component، Props، State و Hook میان آنها مشترکاند، اما عناصر، APIها و روش استایلدهی متفاوت هستند.
تفاوت React و Next.js
| ویژگی | React | Next.js |
|---|---|---|
| نوع ابزار | کتابخانه UI | فریمورک React |
| Routing داخلی | ندارد | دارد |
| Server Rendering | بهتنهایی مدیریت نمیکند | پشتیبانی میکند |
| Backend Route | ندارد | دارد |
| ساختار پروژه | انتخاب توسعهدهنده | ساختار مشخصتر |
| مناسب یادگیری React | بسیار مناسب | لایههای بیشتر |
| مناسب محصول Full Stack | با ابزارهای مکمل | مناسبتر برای بسیاری از پروژهها |
یادگیری خود React پیش از Next.js باعث میشود Component، State، Hook و Rendering را بهتر درک کنید.
پیشنیازهای آموزش
برای اجرای پروژه به این موارد نیاز دارید:
- Node.js
- npm
- یک ویرایشگر مانند VS Code
- مرورگر مدرن
- آشنایی مقدماتی با HTML، CSS و JavaScript
بررسی نصب:
node --version
npm --version
Vite چیست؟
Vite ابزار توسعه و Build برای پروژههای وب مدرن است. با استفاده از Vite میتوان یک پروژه React را سریع ایجاد و در حالت Development اجرا کرد.
طبق راهنمای رسمی Vite، دستور npm create vite@latest برای ساخت پروژه جدید قابلاستفاده است.
ساخت پروژه React:
npm create vite@latest react-ai-assistant -- --template react
ورود به پوشه:
cd react-ai-assistant
نصب وابستگیها:
npm install
اجرای پروژه:
npm run dev
آدرس نمایشدادهشده در Terminal را در مرورگر باز کنید.
ساختار اولیه پروژه React
ساختار معمول پروژه ایجادشده با Vite:
react-ai-assistant/
├── public/
├── src/
│ ├── assets/
│ ├── App.jsx
│ ├── App.css
│ ├── index.css
│ └── main.jsx
├── index.html
├── package.json
└── vite.config.js
نقطه شروع برنامه در main.jsx است:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
createRoot(
document.getElementById("root")
).render(
<StrictMode>
<App />
</StrictMode>
);
عنصر root در فایل index.html قرار دارد:
<div id="root"></div>
React اپلیکیشن را داخل این عنصر Render میکند.
Component در React چیست؟
Component یک تابع JavaScript است که بخشی از رابط کاربری را برمیگرداند.
function Header() {
return (
<header>
<h1>دستیار هوش مصنوعی</h1>
</header>
);
}
استفاده:
function App() {
return (
<div>
<Header />
</div>
);
}
نام Component باید با حرف بزرگ شروع شود:
function Message() {
return <p>پیام</p>;
}
اگر با حرف کوچک بنویسید، React آن را مانند تگ HTML در نظر میگیرد:
function message() {
return <p>پیام</p>;
}
JSX چیست؟
JSX ساختاری شبیه HTML است که داخل JavaScript نوشته میشود:
const element = (
<section>
<h2>مدل انتخابشده</h2>
<p>{modelId}</p>
</section>
);
JSX خود HTML نیست. این Syntax در فرایند Build به فراخوانیهای JavaScript تبدیل میشود.
تفاوتهای JSX و HTML
در JSX بهجای class از className استفاده میشود:
<div className="message">
پیام
</div>
برخی Attributeها با نام Camel Case نوشته میشوند:
<button onClick={handleClick}>
ارسال
</button>
همه تگها باید بسته شوند:
<img src="/logo.png" alt="درواره" />
Component باید یک ریشه واحد برگرداند:
function Profile() {
return (
<>
<h2>پروفایل</h2>
<p>اطلاعات کاربر</p>
</>
);
}
<>...</> یک Fragment است و عنصر اضافی در DOM ایجاد نمیکند.
استفاده از JavaScript در JSX
عبارت JavaScript داخل {} قرار میگیرد:
function ModelInfo() {
const modelId = "YOUR_MODEL_ID";
return (
<p>
مدل فعال: <code>{modelId}</code>
</p>
);
}
محاسبه:
function MessageCount({ messages }) {
return (
<span>
تعداد پیامها: {messages.length}
</span>
);
}
فراخوانی تابع:
function Greeting({ name }) {
return <h2>{name.trim()}</h2>;
}
داخل {} باید Expression قرار گیرد. دستورهایی مانند if مستقیماً در JSX نوشته نمیشوند، اما میتوان نتیجه شرط را پیش از return محاسبه کرد.
Props چیست؟
Props دادههایی هستند که Component والد به Component فرزند ارسال میکند.
function UserMessage({ content }) {
return (
<article className="message message--user">
<strong>شما</strong>
<p>{content}</p>
</article>
);
}
استفاده:
<UserMessage content="React چیست؟" />
چند Prop:
function Message({
role,
content,
createdAt
}) {
return (
<article className={`message message--${role}`}>
<p>{content}</p>
<time>{createdAt}</time>
</article>
);
}
Props فقط خواندنی هستند. Component نباید Prop دریافتی را مستقیماً تغییر دهد.
این کار مناسب نیست:
function Message({ content }) {
content = content.trim();
return <p>{content}</p>;
}
نسخه روشنتر:
function Message({ content }) {
const normalizedContent = content.trim();
return <p>{normalizedContent}</p>;
}
State چیست؟
State اطلاعاتی است که در طول عمر Component تغییر میکند و بر نمایش رابط اثر میگذارد.
برای تعریف State از Hook به نام useState استفاده میکنیم:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button
type="button"
onClick={() => setCount(count + 1)}
>
تعداد: {count}
</button>
);
}
در این مثال:
countمقدار فعلی State است.setCountتابع بهروزرسانی State است.0مقدار اولیه است.
مرجع کامل: مستندات useState در React
بهروزرسانی State براساس مقدار قبلی
اگر مقدار جدید به State قبلی وابسته است، از Updater Function استفاده کنید:
setCount((previousCount) => {
return previousCount + 1;
});
نسخه کوتاه:
setCount((count) => count + 1);
این الگو در بهروزرسانیهای متوالی قابلاعتمادتر است.
State را مستقیماً تغییر ندهید
این کار اشتباه است:
messages.push(newMessage);
setMessages(messages);
آرایه اصلی تغییر کرده و Reference آن ثابت مانده است.
نسخه مناسب:
setMessages((currentMessages) => [
...currentMessages,
newMessage
]);
برای Object:
setSettings((currentSettings) => ({
...currentSettings,
temperature: 0.3
}));
React State را مانند Snapshot در نظر میگیرد. بهتر است Array و Object موجود را تغییر ندهید و مقدار جدید بسازید.
Event در React
Event Handler بهصورت تابع به عنصر داده میشود:
function SendButton() {
function handleClick() {
console.log("پیام ارسال شد");
}
return (
<button
type="button"
onClick={handleClick}
>
ارسال
</button>
);
}
این اشتباه است:
<button onClick={handleClick()}>
ارسال
</button>
در این حالت تابع هنگام Render اجرا میشود.
نسخه صحیح:
<button onClick={handleClick}>
ارسال
</button>
یا:
<button onClick={() => handleClick()}>
ارسال
</button>
فرم کنترلشده در React
در Controlled Component، مقدار Input در State نگهداری میشود:
import { useState } from "react";
function PromptForm() {
const [prompt, setPrompt] = useState("");
function handleSubmit(event) {
event.preventDefault();
const normalizedPrompt = prompt.trim();
if (!normalizedPrompt) {
return;
}
console.log(normalizedPrompt);
setPrompt("");
}
return (
<form onSubmit={handleSubmit}>
<textarea
value={prompt}
onChange={(event) => {
setPrompt(event.target.value);
}}
/>
<button type="submit">
ارسال
</button>
</form>
);
}
در این الگو، React منبع اصلی مقدار Input است.
نمایش شرطی
با if:
function Status({ error, isLoading }) {
if (error) {
return <p className="error">{error}</p>;
}
if (isLoading) {
return <p>در حال دریافت پاسخ...</p>;
}
return <p>آماده</p>;
}
با عملگر سهتایی:
<span>
{isLoading ? "در حال پردازش" : "آماده"}
</span>
با &&:
{error && (
<p className="error">
{error}
</p>
)}
مراقب مقدار صفر باشید:
// ممکن است عدد صفر روی صفحه نمایش داده شود
{messages.length && <MessageList />}
نسخه بهتر:
{messages.length > 0 && <MessageList />}
نمایش فهرست با map
function MessageList({ messages }) {
return (
<section>
{messages.map((message) => (
<Message
key={message.id}
role={message.role}
content={message.content}
/>
))}
</section>
);
}
اهمیت key در React
هر عضو فهرست باید یک key پایدار و یکتا داشته باشد:
<Message
key={message.id}
content={message.content}
/>
در فهرستی که ترتیب آن تغییر میکند، استفاده از Index انتخاب مناسبی نیست:
messages.map((message, index) => (
<Message
key={index}
content={message.content}
/>
));
بهتر است هر پیام هنگام ایجاد ID داشته باشد:
const message = {
id: crypto.randomUUID(),
role: "user",
content: prompt
};
key برای React است و بهعنوان Prop معمولی در Component دریافت نمیشود. اگر ID را نیاز دارید، جداگانه ارسال کنید:
<Message
key={message.id}
messageId={message.id}
/>
بالا بردن State یا Lifting State Up
اگر چند Component به یک داده مشترک نیاز دارند، State را در نزدیکترین والد مشترک قرار دهید.
function App() {
const [messages, setMessages] = useState([]);
return (
<>
<MessageList messages={messages} />
<PromptForm
onSend={(message) => {
setMessages((current) => [
...current,
message
]);
}}
/>
</>
);
}
بهاینترتیب:
Appمالک State است.MessageListداده را دریافت میکند.PromptFormرویداد را به والد اطلاع میدهد.
Hook چیست؟
Hook تابعی ویژه در React است که امکان استفاده از قابلیتهایی مانند State، Context و Lifecycle را در Function Component فراهم میکند.
Hookهای پرکاربرد:
useStateuseEffectuseRefuseMemouseCallbackuseReduceruseContext
قواعد اصلی Hook:
- Hook را در بالاترین سطح Component فراخوانی کنید.
- Hook را داخل شرط، حلقه یا تابع تودرتو فراخوانی نکنید.
- Hook را فقط در React Component یا Custom Hook فراخوانی کنید.
- نام Custom Hook را با
useشروع کنید.
این کار اشتباه است:
if (isEnabled) {
const [value, setValue] = useState("");
}
نسخه مناسب:
const [value, setValue] = useState("");
if (!isEnabled) {
return null;
}
useEffect چیست؟
useEffect برای هماهنگکردن Component با یک سیستم خارجی استفاده میشود؛ مانند:
- درخواست شبکه
- Event Listener مرورگر
- Timer
- اتصال WebSocket
- API غیرReact
- همگامسازی عنوان صفحه
مثال:
import { useEffect } from "react";
function PageTitle({ messageCount }) {
useEffect(() => {
document.title =
`${messageCount} پیام | دستیار درواره`;
}, [messageCount]);
return null;
}
Effect پس از Commit شدن تغییرات اجرا میشود.
مرجع رسمی: مستندات useEffect در React
آیا برای هر محاسبهای به useEffect نیاز داریم؟
خیر. دادهای که مستقیماً از Props یا State قابلمحاسبه است، معمولاً نباید State جدا یا Effect داشته باشد.
روش غیرضروری:
const [messageCount, setMessageCount] =
useState(0);
useEffect(() => {
setMessageCount(messages.length);
}, [messages]);
نسخه سادهتر:
const messageCount = messages.length;
Effect برای هماهنگی با سیستم خارجی است، نه هر محاسبه داخل Component.
Cleanup در useEffect
برای Event Listener یا Timer باید Cleanup تعریف شود:
useEffect(() => {
function handleOnline() {
console.log("اتصال برقرار شد");
}
window.addEventListener(
"online",
handleOnline
);
return () => {
window.removeEventListener(
"online",
handleOnline
);
};
}, []);
برای Timer:
useEffect(() => {
const timerId = setInterval(() => {
console.log("checking...");
}, 5000);
return () => {
clearInterval(timerId);
};
}, []);
useRef چیست؟
useRef مقدار قابلتغییری را نگهداری میکند که تغییر آن باعث Render مجدد نمیشود. همچنین برای دسترسی به عنصر DOM کاربرد دارد.
Focus روی Textarea:
import { useRef } from "react";
function PromptForm() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current?.focus();
}
return (
<>
<textarea ref={inputRef} />
<button
type="button"
onClick={focusInput}
>
نوشتن پیام
</button>
</>
);
}
نگهداری AbortController نیز کاربرد مناسبی برای Ref است:
const controllerRef = useRef(null);
useReducer چیست؟
وقتی State چند حالت و Transition مرتبط دارد، useReducer میتواند منطق را منظمتر کند.
import { useReducer } from "react";
const initialState = {
messages: [],
isLoading: false,
error: ""
};
function chatReducer(state, action) {
switch (action.type) {
case "message/added":
return {
...state,
messages: [
...state.messages,
action.payload
]
};
case "request/started":
return {
...state,
isLoading: true,
error: ""
};
case "request/succeeded":
return {
...state,
isLoading: false,
messages: [
...state.messages,
action.payload
]
};
case "request/failed":
return {
...state,
isLoading: false,
error: action.payload
};
default:
throw new Error(
`Unknown action: ${action.type}`
);
}
}
استفاده:
const [state, dispatch] = useReducer(
chatReducer,
initialState
);
برای یک Boolean ساده، useState مناسبتر است. برای Workflowهایی با حالتهای مرتبط، Reducer میتواند قابلفهمتر باشد.
Context چیست؟
Context امکان ارسال داده به Componentهای عمیق را بدون عبور دستی Prop از تمام لایهها فراهم میکند.
موارد مناسب:
- Theme
- زبان رابط
- اطلاعات کاربر احراز هویتشده
- تنظیمات عمومی
- وابستگی مشترک محدود
Context نباید بدون نیاز برای تمام Stateهای برنامه استفاده شود. State نزدیک به محل مصرف معمولاً سادهتر و قابلنگهداریتر است.
Custom Hook چیست؟
Custom Hook منطق React قابلاستفاده مجدد را استخراج میکند.
import {
useEffect,
useState
} from "react";
export function useOnlineStatus() {
const [isOnline, setIsOnline] =
useState(navigator.onLine);
useEffect(() => {
function updateStatus() {
setIsOnline(navigator.onLine);
}
window.addEventListener(
"online",
updateStatus
);
window.addEventListener(
"offline",
updateStatus
);
return () => {
window.removeEventListener(
"online",
updateStatus
);
window.removeEventListener(
"offline",
updateStatus
);
};
}, []);
return isOnline;
}
استفاده:
function ConnectionStatus() {
const isOnline = useOnlineStatus();
return (
<span>
{isOnline ? "آنلاین" : "آفلاین"}
</span>
);
}
Custom Hook رابط کاربری برنمیگرداند؛ منطق و State قابلاشتراک را فراهم میکند.
تفکر Component-based
براساس راهنمای Thinking in React، یک روش مناسب طراحی رابط این است:
- رابط را به Componentهای کوچک تقسیم کنید.
- نسخه ثابت رابط را بسازید.
- حداقل State موردنیاز را مشخص کنید.
- مالک مناسب هر State را پیدا کنید.
- جریان داده و Eventها را طراحی کنید.
برای دستیار هوش مصنوعی میتوانیم این ساختار را داشته باشیم:
App
├── AppHeader
├── ModelInfo
├── MessageList
│ └── MessageItem
├── EmptyState
├── ErrorMessage
└── PromptForm
پروژه عملی: ساخت اپلیکیشن هوش مصنوعی با React
پروژه نهایی شامل این قابلیتها است:
- نمایش پیامهای کاربر و دستیار
- فرم کنترلشده
- مدیریت State با
useReducer - Componentهای مستقل
- Custom Hook برای گفتوگو
- ارتباط با Backend
- مدیریت Loading
- مدیریت خطا
- توقف درخواست
- پاککردن گفتوگو
- Scroll خودکار
- جلوگیری از افشای API Key
- طراحی فارسی و RTL
معماری:
React در مرورگر
↓
Backend برنامه
↓
API درواره
↓
مدل هوش مصنوعی
ساختار نهایی پروژه
react-ai-assistant/
├── backend/
│ ├── .env
│ ├── package.json
│ └── server.js
├── src/
│ ├── components/
│ │ ├── AppHeader.jsx
│ │ ├── MessageItem.jsx
│ │ ├── MessageList.jsx
│ │ └── PromptForm.jsx
│ ├── hooks/
│ │ └── useChat.js
│ ├── services/
│ │ └── chatApi.js
│ ├── App.jsx
│ ├── App.css
│ ├── index.css
│ └── main.jsx
├── .gitignore
├── index.html
├── package.json
└── vite.config.js
تنظیم Proxy در Vite
Frontend روی یک Port و Backend روی Port دیگری اجرا میشوند. در Development میتوانیم درخواستهای /api را با Proxy به Backend بفرستیم.
فایل vite.config.js:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
react()
],
server: {
proxy: {
"/api": {
target: "http://localhost:3001",
changeOrigin: true
}
}
}
});
در Frontend درخواست را به مسیر نسبی میفرستیم:
fetch("/api/chat");
کلید API همچنان در Backend قرار دارد. Proxy فقط مسیر درخواست Development را ساده میکند.
ساخت Service ارتباط با Backend
فایل src/services/chatApi.js:
export async function sendChatRequest({
messages,
signal
}) {
const response = await fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
messages
}),
signal
});
const contentType =
response.headers.get("content-type") || "";
if (!contentType.includes("application/json")) {
throw new Error(
"پاسخ سرور در قالب مورد انتظار نیست."
);
}
const data = await response.json();
if (!response.ok) {
throw new Error(
data.error ||
`درخواست ناموفق بود. کد: ${response.status}`
);
}
if (
typeof data.answer !== "string" ||
!data.answer.trim()
) {
throw new Error(
"پاسخ معتبر از مدل دریافت نشد."
);
}
return data.answer;
}
قرار دادن منطق شبکه در Service چند مزیت دارد:
- Component سادهتر میشود.
- تست ارتباط راحتتر است.
- Endpoint در یک محل مدیریت میشود.
- منطق Parse کردن پاسخ تکرار نمیشود.
ساخت Custom Hook گفتوگو
فایل src/hooks/useChat.js:
import {
useCallback,
useReducer,
useRef
} from "react";
import {
sendChatRequest
} from "../services/chatApi.js";
const initialState = {
messages: [],
isLoading: false,
error: ""
};
function createMessage(role, content) {
return {
id: crypto.randomUUID(),
role,
content,
createdAt: new Date().toISOString()
};
}
function chatReducer(state, action) {
switch (action.type) {
case "request/started":
return {
...state,
isLoading: true,
error: "",
messages: [
...state.messages,
action.payload
]
};
case "request/succeeded":
return {
...state,
isLoading: false,
messages: [
...state.messages,
action.payload
]
};
case "request/failed":
return {
...state,
isLoading: false,
error: action.payload
};
case "request/cancelled":
return {
...state,
isLoading: false,
error: "درخواست متوقف شد."
};
case "chat/cleared":
return initialState;
default:
throw new Error(
`Unknown action type: ${action.type}`
);
}
}
function getApiMessages(messages) {
return messages
.slice(-20)
.map(({ role, content }) => ({
role,
content
}));
}
export function useChat() {
const [state, dispatch] = useReducer(
chatReducer,
initialState
);
const controllerRef = useRef(null);
const sendMessage = useCallback(
async (rawPrompt) => {
const prompt = rawPrompt.trim();
if (!prompt) {
throw new Error(
"پیام نمیتواند خالی باشد."
);
}
if (prompt.length > 4000) {
throw new Error(
"پیام نباید بیشتر از ۴۰۰۰ کاراکتر باشد."
);
}
if (state.isLoading) {
return;
}
const userMessage =
createMessage("user", prompt);
const requestMessages = [
...state.messages,
userMessage
];
dispatch({
type: "request/started",
payload: userMessage
});
const controller =
new AbortController();
controllerRef.current = controller;
try {
const answer = await sendChatRequest({
messages:
getApiMessages(requestMessages),
signal: controller.signal
});
dispatch({
type: "request/succeeded",
payload: createMessage(
"assistant",
answer
)
});
} catch (error) {
if (error.name === "AbortError") {
dispatch({
type: "request/cancelled"
});
return;
}
dispatch({
type: "request/failed",
payload:
error.message ||
"خطای غیرمنتظره رخ داد."
});
} finally {
controllerRef.current = null;
}
},
[
state.isLoading,
state.messages
]
);
const cancelRequest = useCallback(() => {
controllerRef.current?.abort();
}, []);
const clearChat = useCallback(() => {
controllerRef.current?.abort();
dispatch({
type: "chat/cleared"
});
}, []);
return {
...state,
sendMessage,
cancelRequest,
clearChat
};
}
این Hook مسئول State و منطق گفتوگو است. Componentهای رابط لازم نیست جزئیات fetch یا AbortController را بدانند.
ساخت Header
فایل src/components/AppHeader.jsx:
export function AppHeader({
messageCount,
isLoading,
onClear
}) {
return (
<header className="app-header">
<div>
<p className="eyebrow">
پروژه عملی React
</p>
<h1>
دستیار هوش مصنوعی درواره
</h1>
<p className="app-header__meta">
{messageCount} پیام در گفتوگو
</p>
</div>
<div className="app-header__actions">
<span
className={
isLoading
? "status status--loading"
: "status"
}
>
{isLoading
? "در حال پردازش"
: "آماده"}
</span>
<button
className="button button--ghost"
type="button"
onClick={onClear}
disabled={
isLoading || messageCount === 0
}
>
گفتوگوی جدید
</button>
</div>
</header>
);
}
ساخت Component پیام
فایل src/components/MessageItem.jsx:
function formatTime(value) {
return new Intl.DateTimeFormat("fa-IR", {
hour: "2-digit",
minute: "2-digit"
}).format(new Date(value));
}
export function MessageItem({
role,
content,
createdAt
}) {
const isUser = role === "user";
return (
<article
className={
isUser
? "message message--user"
: "message message--assistant"
}
>
<div
className="message__avatar"
aria-hidden="true"
>
{isUser ? "ش" : "د"}
</div>
<div className="message__body">
<div className="message__meta">
<strong>
{isUser
? "شما"
: "دستیار درواره"}
</strong>
<time dateTime={createdAt}>
{formatTime(createdAt)}
</time>
</div>
<p className="message__content">
{content}
</p>
</div>
</article>
);
}
React متن content را بهصورت متن نمایش میدهد و آن را مانند innerHTML مستقیماً تفسیر نمیکند. برای نمایش Markdown باید یک راهکار معتبر همراه با پاکسازی HTML انتخاب شود.
ساخت فهرست پیامها
فایل src/components/MessageList.jsx:
import {
useEffect,
useRef
} from "react";
import {
MessageItem
} from "./MessageItem.jsx";
export function MessageList({
messages,
isLoading
}) {
const endRef = useRef(null);
useEffect(() => {
endRef.current?.scrollIntoView({
behavior: "smooth",
block: "end"
});
}, [
messages,
isLoading
]);
if (messages.length === 0) {
return (
<section className="empty-state">
<div
className="empty-state__icon"
aria-hidden="true"
>
✦
</div>
<h2>
اولین پیام را ارسال کنید
</h2>
<p>
میتوانید درباره برنامهنویسی، تولید محتوا
یا تحلیل متن سؤال بپرسید.
</p>
</section>
);
}
return (
<section
className="message-list"
aria-label="پیامهای گفتوگو"
aria-live="polite"
>
{messages.map((message) => (
<MessageItem
key={message.id}
role={message.role}
content={message.content}
createdAt={message.createdAt}
/>
))}
{isLoading && (
<article className="message message--assistant">
<div
className="message__avatar"
aria-hidden="true"
>
د
</div>
<div className="typing">
<span></span>
<span></span>
<span></span>
<span className="sr-only">
در حال آمادهسازی پاسخ
</span>
</div>
</article>
)}
<div ref={endRef}></div>
</section>
);
}
ساخت فرم ارسال پیام
فایل src/components/PromptForm.jsx:
import {
useRef,
useState
} from "react";
export function PromptForm({
isLoading,
onSend,
onCancel
}) {
const [prompt, setPrompt] = useState("");
const [validationError, setValidationError] =
useState("");
const inputRef = useRef(null);
async function handleSubmit(event) {
event.preventDefault();
const normalizedPrompt = prompt.trim();
if (!normalizedPrompt) {
setValidationError(
"لطفاً متن پیام را وارد کنید."
);
inputRef.current?.focus();
return;
}
if (normalizedPrompt.length > 4000) {
setValidationError(
"پیام نباید بیشتر از ۴۰۰۰ کاراکتر باشد."
);
return;
}
setValidationError("");
setPrompt("");
try {
await onSend(normalizedPrompt);
} catch (error) {
setValidationError(error.message);
setPrompt(normalizedPrompt);
} finally {
inputRef.current?.focus();
}
}
function handleKeyDown(event) {
if (
event.key === "Enter" &&
!event.shiftKey &&
!event.nativeEvent.isComposing
) {
event.preventDefault();
event.currentTarget.form?.requestSubmit();
}
}
return (
<form
className="prompt-form"
onSubmit={handleSubmit}
>
<label
className="sr-only"
htmlFor="prompt"
>
پیام شما
</label>
<textarea
ref={inputRef}
id="prompt"
name="prompt"
rows="2"
maxLength="4000"
value={prompt}
placeholder="پیام خود را بنویسید..."
disabled={isLoading}
onChange={(event) => {
setPrompt(event.target.value);
if (validationError) {
setValidationError("");
}
}}
onKeyDown={handleKeyDown}
/>
<div className="prompt-form__footer">
<span className="character-count">
{prompt.length.toLocaleString("fa-IR")}
{" / "}
۴۰۰۰
</span>
<div className="prompt-form__actions">
{isLoading && (
<button
className="button button--secondary"
type="button"
onClick={onCancel}
>
توقف
</button>
)}
<button
className="button button--primary"
type="submit"
disabled={
isLoading || !prompt.trim()
}
>
ارسال پیام
</button>
</div>
</div>
{validationError && (
<p
className="form-error"
role="alert"
>
{validationError}
</p>
)}
</form>
);
}
ساخت Component اصلی App
فایل src/App.jsx:
import {
AppHeader
} from "./components/AppHeader.jsx";
import {
MessageList
} from "./components/MessageList.jsx";
import {
PromptForm
} from "./components/PromptForm.jsx";
import {
useChat
} from "./hooks/useChat.js";
import "./App.css";
export default function App() {
const {
messages,
isLoading,
error,
sendMessage,
cancelRequest,
clearChat
} = useChat();
return (
<div className="page-shell">
<main className="chat-app">
<AppHeader
messageCount={messages.length}
isLoading={isLoading}
onClear={clearChat}
/>
<MessageList
messages={messages}
isLoading={isLoading}
/>
{error && (
<p
className="request-error"
role="alert"
>
{error}
</p>
)}
<PromptForm
isLoading={isLoading}
onSend={sendMessage}
onCancel={cancelRequest}
/>
<p className="disclosure">
پاسخ هوش مصنوعی ممکن است نیازمند بررسی باشد.
</p>
</main>
</div>
);
}
استایل عمومی
فایل src/index.css:
:root {
font-family:
Vazirmatn,
Tahoma,
Arial,
sans-serif;
color: #172033;
background-color: #f4f5fa;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
min-width: 320px;
min-height: 100%;
}
body {
min-width: 320px;
min-height: 100dvh;
margin: 0;
}
button,
textarea {
font: inherit;
}
button {
border: 0;
}
button:focus-visible,
textarea:focus-visible {
outline: 3px solid rgb(109 74 255 / 28%);
outline-offset: 3px;
}
استایل اپلیکیشن
فایل src/App.css:
:root {
--primary: #6d4aff;
--primary-dark: #5536df;
--primary-soft: #f0edff;
--text: #172033;
--muted: #697386;
--border: #e2e6ef;
--surface: #ffffff;
--page: #f4f5fa;
--error: #b42318;
--radius-sm: 10px;
--radius-md: 16px;
--radius-lg: 24px;
}
.page-shell {
min-height: 100dvh;
padding: 24px;
background:
radial-gradient(
circle at 90% 5%,
rgb(109 74 255 / 12%),
transparent 28%
),
var(--page);
}
.chat-app {
display: grid;
grid-template-rows:
auto
minmax(300px, 1fr)
auto
auto
auto;
width: min(100%, 960px);
min-height: calc(100dvh - 48px);
margin-inline: auto;
background-color: rgb(255 255 255 / 92%);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow:
0 28px 80px rgb(20 24 40 / 9%);
overflow: hidden;
}
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 22px 26px;
border-block-end:
1px solid var(--border);
}
.app-header h1 {
margin: 0;
font-size: clamp(1.25rem, 3vw, 1.8rem);
}
.eyebrow {
margin: 0 0 3px;
color: var(--primary);
font-size: 0.75rem;
font-weight: 800;
}
.app-header__meta {
margin: 4px 0 0;
color: var(--muted);
font-size: 0.75rem;
}
.app-header__actions {
display: flex;
align-items: center;
gap: 10px;
}
.status {
padding: 6px 11px;
color: #087443;
background-color: #ecfdf3;
border: 1px solid #abefc6;
border-radius: 999px;
font-size: 0.75rem;
white-space: nowrap;
}
.status--loading {
color: #6941c6;
background-color: #f4f0ff;
border-color: #d9d0ff;
}
.message-list {
min-height: 0;
padding: clamp(20px, 4vw, 40px);
background-color: #f8f9fc;
overflow-y: auto;
overscroll-behavior: contain;
scroll-behavior: smooth;
}
.message {
display: flex;
align-items: flex-start;
gap: 10px;
margin-block-end: 20px;
}
.message--user {
flex-direction: row-reverse;
}
.message__avatar {
display: grid;
place-items: center;
flex: 0 0 auto;
width: 38px;
height: 38px;
color: #ffffff;
background-color: var(--primary);
border-radius: 12px;
font-weight: 800;
}
.message--user .message__avatar {
color: var(--primary);
background-color: var(--primary-soft);
}
.message__body {
max-width: min(78%, 720px);
}
.message--user .message__body {
margin-inline-start: 0;
}
.message__meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin: 0 5px 5px;
color: var(--muted);
font-size: 0.7rem;
}
.message__content {
margin: 0;
padding: 13px 16px;
background-color: var(--surface);
border: 1px solid var(--border);
border-radius:
5px
var(--radius-md)
var(--radius-md)
var(--radius-md);
box-shadow:
0 10px 28px rgb(20 24 40 / 5%);
line-height: 1.9;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.message--user .message__content {
color: #ffffff;
background-color: var(--primary);
border-color: var(--primary);
border-radius:
var(--radius-md)
5px
var(--radius-md)
var(--radius-md);
}
.empty-state {
align-self: center;
max-width: 480px;
margin: auto;
padding: 40px 20px;
text-align: center;
}
.empty-state__icon {
display: grid;
place-items: center;
width: 64px;
height: 64px;
margin: 0 auto 16px;
color: var(--primary);
background-color: var(--primary-soft);
border-radius: 20px;
font-size: 1.7rem;
}
.empty-state h2 {
margin: 0;
}
.empty-state p {
margin: 8px 0 0;
color: var(--muted);
}
.typing {
display: flex;
align-items: center;
gap: 5px;
min-height: 42px;
padding: 12px 16px;
background-color: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
.typing > span:not(.sr-only) {
width: 7px;
height: 7px;
background-color: var(--primary);
border-radius: 50%;
animation:
typing-pulse 1s ease-in-out infinite;
}
.typing > span:nth-child(2) {
animation-delay: 120ms;
}
.typing > span:nth-child(3) {
animation-delay: 240ms;
}
@keyframes typing-pulse {
0%,
100% {
opacity: 0.3;
transform: translateY(0);
}
50% {
opacity: 1;
transform: translateY(-3px);
}
}
.prompt-form {
margin: 16px 20px 8px;
padding: 8px;
background-color: var(--surface);
border: 1px solid var(--border);
border-radius: 18px;
box-shadow:
0 18px 45px rgb(20 24 40 / 8%);
}
.prompt-form:focus-within {
border-color: rgb(109 74 255 / 55%);
box-shadow:
0 0 0 4px rgb(109 74 255 / 8%),
0 18px 45px rgb(20 24 40 / 8%);
}
.prompt-form textarea {
width: 100%;
min-height: 74px;
max-height: 200px;
padding: 10px 12px;
color: var(--text);
background-color: transparent;
border: 0;
outline: 0;
resize: vertical;
line-height: 1.8;
}
.prompt-form textarea::placeholder {
color: #98a2b3;
}
.prompt-form__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 4px;
}
.prompt-form__actions {
display: flex;
gap: 8px;
}
.character-count {
color: var(--muted);
font-size: 0.7rem;
}
.button {
min-height: 42px;
padding: 8px 15px;
border-radius: 11px;
cursor: pointer;
transition:
opacity 150ms ease,
background-color 150ms ease,
transform 150ms ease;
}
.button:hover:not(:disabled) {
transform: translateY(-1px);
}
.button:disabled {
opacity: 0.48;
cursor: not-allowed;
}
.button--primary {
color: #ffffff;
background-color: var(--primary);
}
.button--primary:hover:not(:disabled) {
background-color: var(--primary-dark);
}
.button--secondary,
.button--ghost {
color: var(--text);
background-color: #eef0f5;
}
.form-error,
.request-error {
color: var(--error);
font-size: 0.78rem;
}
.form-error {
margin: 7px 5px 2px;
}
.request-error {
margin: 10px 24px 0;
padding: 10px 12px;
background-color: #fef3f2;
border: 1px solid #fecdca;
border-radius: var(--radius-sm);
}
.disclosure {
margin: 0;
padding: 4px 20px 14px;
color: var(--muted);
font-size: 0.7rem;
text-align: center;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media (max-width: 680px) {
.page-shell {
padding: 0;
}
.chat-app {
min-height: 100dvh;
border: 0;
border-radius: 0;
}
.app-header {
align-items: flex-start;
padding: 18px;
}
.app-header__actions {
flex-direction: column;
align-items: flex-end;
}
.message-list {
padding: 20px 14px;
}
.message__body {
max-width: calc(100% - 46px);
}
.prompt-form {
margin-inline: 12px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
ساخت Backend امن
کلید API نباید در کد React یا متغیرهای قابلارسال Vite قرار گیرد. هر مقداری که وارد Bundle مرورگر شود برای کاربر قابلمشاهده خواهد بود.
پوشه Backend را بسازید:
mkdir backend
cd backend
npm init -y
npm install express dotenv
فایل backend/package.json:
{
"name": "react-ai-assistant-backend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"dotenv": "^17.0.0",
"express": "^5.0.0"
}
}
نسخه دقیق وابستگیها را همان نسخهای در نظر بگیرید که npm هنگام نصب در package.json و Lock File ثبت میکند.
فایل backend/server.js:
import "dotenv/config";
import express from "express";
const app = express();
const port = Number(
process.env.PORT || 3001
);
const apiKey =
process.env.DARVAREH_API_KEY;
const modelId =
process.env.DARVAREH_MODEL_ID;
const chatUrl =
"https://api.darvareh.ir/v1/chat/completions";
app.disable("x-powered-by");
app.use(
express.json({
limit: "64kb"
})
);
function isValidMessage(message) {
return (
message !== null &&
typeof message === "object" &&
["user", "assistant"].includes(message.role) &&
typeof message.content === "string" &&
message.content.trim().length > 0 &&
message.content.length <= 10000
);
}
function validateMessages(messages) {
return (
Array.isArray(messages) &&
messages.length > 0 &&
messages.length <= 20 &&
messages.every(isValidMessage)
);
}
function extractAnswer(data) {
const answer =
data?.choices?.[0]?.message?.content;
if (
typeof answer !== "string" ||
!answer.trim()
) {
throw new Error(
"Invalid model response"
);
}
return answer;
}
app.post("/api/chat", async (request, response) => {
if (!apiKey || !modelId) {
return response.status(500).json({
error:
"تنظیمات API در سرور کامل نیست."
});
}
const { messages } = request.body ?? {};
if (!validateMessages(messages)) {
return response.status(400).json({
error:
"ساختار پیامها معتبر نیست."
});
}
const controller =
new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, 60000);
try {
const upstreamResponse = await fetch(
chatUrl,
{
method: "POST",
headers: {
"Authorization":
`Bearer ${apiKey}`,
"Content-Type":
"application/json"
},
body: JSON.stringify({
model: modelId,
messages: [
{
role: "system",
content:
"شما یک دستیار فارسی، دقیق و مفید هستید. " +
"پاسخ را روشن، کاربردی و ساختاریافته ارائه کنید."
},
...messages
],
temperature: 0.4,
max_tokens: 1200
}),
signal: controller.signal
}
);
const rawBody =
await upstreamResponse.text();
let data;
try {
data = JSON.parse(rawBody);
} catch {
return response.status(502).json({
error:
"پاسخ سرویس مدل قابل پردازش نبود."
});
}
if (!upstreamResponse.ok) {
console.error(
"Upstream request failed:",
upstreamResponse.status
);
return response.status(502).json({
error:
"سرویس مدل پاسخ موفقی ارسال نکرد."
});
}
return response.json({
answer: extractAnswer(data)
});
} catch (error) {
if (error.name === "AbortError") {
return response.status(504).json({
error:
"زمان انتظار برای پاسخ به پایان رسید."
});
}
console.error(
"Chat request failed:",
error.message
);
return response.status(502).json({
error:
"ارتباط با سرویس مدل برقرار نشد."
});
} finally {
clearTimeout(timeoutId);
}
});
app.listen(port, () => {
console.log(
`Backend: http://localhost:${port}`
);
});
تنظیم متغیرهای محیطی Backend
فایل backend/.env:
PORT=3001
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID
برای دریافت API Key میتوانید در درواره ثبتنام کنید.
برای مشاهده مدلهای قابلاستفاده، انتخاب Model ID و بررسی قیمت بهروز هر مدل به صفحه مدلهای درواره مراجعه کنید.
فایل .env را وارد Git نکنید:
node_modules/
dist/
.env
backend/.env
اجرای کامل پروژه
Terminal اول برای Backend:
cd backend
npm run dev
Terminal دوم برای Frontend:
npm run dev
Frontend معمولاً روی آدرس اعلامشده توسط Vite اجرا میشود و درخواست /api/chat از طریق Proxy به Backend میرسد.
چرا API Key را در VITE_ قرار نمیدهیم؟
در Vite، متغیرهایی که برای Frontend در دسترس قرار میگیرند معمولاً با پیشوند VITE_ تعریف میشوند:
VITE_PUBLIC_API_URL=/api
این متغیرها در Bundle مرورگر قابلمشاهدهاند. بنابراین این کار مناسب نیست:
VITE_DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
پیشوند VITE_ یک Secret Manager نیست. کلید باید فقط در Backend قرار گیرد:
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
معماری مناسب:
React Frontend
↓ بدون Secret
Backend
↓ همراه API Key
API درواره
StrictMode و اجرای دوباره Effect
در حالت Development ممکن است React Strict Mode بعضی از فرایندها را بیش از یکبار اجرا کند تا مشکلات Cleanup و Side Effectها مشخص شوند.
اگر یک Effect در Development دو بار اجرا میشود، فوراً StrictMode را حذف نکنید. ابتدا بررسی کنید:
- آیا Effect دارای Cleanup صحیح است؟
- آیا درخواست شبکه باید در Effect انجام شود؟
- آیا عملیات Idempotent است؟
- آیا State مشتقشده را بیدلیل در Effect تنظیم کردهاید؟
- آیا اتصال خارجی پس از Unmount قطع میشود؟
در نسخه Production رفتار Development Strict Mode به همان شکل تکرار نمیشود، اما کد باید همچنان Cleanup صحیح داشته باشد.
اشتباههای رایج در React
تغییر مستقیم State
اشتباه:
messages.push(newMessage);
setMessages(messages);
صحیح:
setMessages((messages) => [
...messages,
newMessage
]);
استفاده نادرست از Event Handler
اشتباه:
<button onClick={clearChat()}>
پاککردن
</button>
صحیح:
<button onClick={clearChat}>
پاککردن
</button>
استفاده از Index بهعنوان key
اشتباه در فهرست قابلتغییر:
messages.map((message, index) => (
<MessageItem
key={index}
{...message}
/>
));
بهتر:
messages.map((message) => (
<MessageItem
key={message.id}
{...message}
/>
));
نگهداری State مشتقشده
غیرضروری:
const [count, setCount] = useState(0);
useEffect(() => {
setCount(messages.length);
}, [messages]);
سادهتر:
const count = messages.length;
استفاده بیش از حد از useEffect
اگر عملیاتی با کلیک کاربر انجام میشود، معمولاً آن را در Event Handler قرار دهید:
async function handleSubmit(event) {
event.preventDefault();
await sendMessage(prompt);
}
بهجای اینکه تغییر State را با Effect دنبال و سپس درخواست را ارسال کنید.
تعریف Component داخل Component
این ساختار ممکن است در هر Render نوع Component جدیدی بسازد:
function App() {
function Message() {
return <p>پیام</p>;
}
return <Message />;
}
بهتر است Component مستقل باشد:
function Message() {
return <p>پیام</p>;
}
function App() {
return <Message />;
}
قرار دادن تمام State در Context
Context برای دادههای مشترک مفید است، اما تغییر مکرر Context میتواند Componentهای زیادی را درگیر Render کند. State را تا حد ممکن نزدیک محل مصرف نگه دارید.
ذخیره JSX در State
معمولاً داده را در State نگهداری کنید و JSX را از روی داده بسازید:
const [messages, setMessages] = useState([]);
نه اینکه عناصر React را مستقیماً ذخیره کنید.
قراردادن API Key در React
هیچ Secret واقعی نباید در این موارد قرار گیرد:
- فایل JSX
- فایل JavaScript مرورگر
- متغیر
VITE_ - Local Storage
- Session Storage
- فایل عمومی Config
- Repository عمومی
مدیریت State در پروژه بزرگ
برای پروژه کوچک، useState و useReducer کافی هستند.
با بزرگشدن برنامه ممکن است به ابزارهای دیگری نیاز داشته باشید:
| نیاز | گزینه |
|---|---|
| State محلی | useState |
| Workflow پیچیده محلی | useReducer |
| داده عمومی محدود | Context |
| داده Server و Cache | کتابخانه Query |
| State سراسری پیچیده | کتابخانه State Management |
| State داخل URL | Router و Search Params |
| فرم بزرگ | کتابخانه Form |
قبل از نصب ابزار جدید مشخص کنید State متعلق به کدام دسته است:
- State رابط
- State فرم
- State سرور
- State URL
- State احراز هویت
- State سراسری محصول
همه این موارد نباید در یک Store واحد قرار گیرند.
بهینهسازی Render در React
در بیشتر پروژهها ابتدا کد ساده و صحیح بنویسید و سپس با اندازهگیری بهینهسازی کنید.
ابزارهای رایج:
React.memouseMemouseCallback- تقسیم Component
- نزدیک نگهداشتن State به محل مصرف
- Virtualization برای فهرست بسیار بزرگ
- Lazy Loading
- Code Splitting
useMemo و useCallback نباید بهصورت خودکار در تمام Componentها استفاده شوند. آنها نیز پیچیدگی و هزینه نگهداری دارند.
نمونه محاسبه پرهزینه:
const filteredMessages = useMemo(() => {
return messages.filter((message) =>
message.content.includes(searchQuery)
);
}, [
messages,
searchQuery
]);
اگر محاسبه ساده است، Memoization ممکن است ضرورتی نداشته باشد.
Lazy Loading
برای بارگذاری Component سنگین در زمان نیاز:
import {
lazy,
Suspense
} from "react";
const SettingsPanel = lazy(() => {
return import("./SettingsPanel.jsx");
});
function App() {
return (
<Suspense fallback={<p>در حال بارگذاری...</p>}>
<SettingsPanel />
</Suspense>
);
}
این روش میتواند Bundle اولیه را کاهش دهد، اما باید براساس ساختار واقعی صفحه استفاده شود.
تست Component
ابزارهای متداول تست React میتوانند رفتار قابلمشاهده کاربر را بررسی کنند.
نمونه مفهومی:
import {
render,
screen
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
PromptForm
} from "./PromptForm.jsx";
test("sends a valid prompt", async () => {
const user = userEvent.setup();
const onSend = vi.fn();
render(
<PromptForm
isLoading={false}
onSend={onSend}
onCancel={() => {}}
/>
);
await user.type(
screen.getByLabelText("پیام شما"),
"React چیست؟"
);
await user.click(
screen.getByRole("button", {
name: "ارسال پیام"
})
);
expect(onSend).toHaveBeenCalledWith(
"React چیست؟"
);
});
بهتر است بهجای جزئیات داخلی Component، رفتار قابلمشاهده کاربر را تست کنید:
- آیا فرم ارسال میشود؟
- آیا پیام خطا نمایش داده میشود؟
- آیا دکمه هنگام Loading غیرفعال است؟
- آیا پیام جدید روی صفحه دیده میشود؟
- آیا امکان لغو درخواست وجود دارد؟
Build پروژه
برای ساخت نسخه Production:
npm run build
خروجی معمولاً داخل پوشه dist قرار میگیرد:
dist/
├── assets/
└── index.html
بررسی محلی Build:
npm run preview
preview جای سرور Production را نمیگیرد و برای بررسی خروجی Build استفاده میشود.
در استقرار نهایی باید مشخص کنید:
- فایلهای Frontend کجا سرو میشوند؟
- Backend کجا اجرا میشود؟
- مسیر
/apiچگونه Route میشود؟ - HTTPS چگونه فعال میشود؟
- Secretها کجا نگهداری میشوند؟
- Log و Monitoring چگونه انجام میشوند؟
- محدودیت درخواست چگونه اعمال میشود؟
چکلیست React برای پروژه واقعی
- Componentها مسئولیت مشخص دارند.
- Props فقط خوانده میشوند.
- State مستقیماً تغییر نمیکند.
- فهرستها
keyپایدار دارند. - State مشتقشده بیدلیل ذخیره نشده است.
- Effect فقط برای هماهنگی با سیستم خارجی استفاده میشود.
- Effectهای لازم Cleanup دارند.
- Hook داخل شرط یا حلقه فراخوانی نمیشود.
- فرم ورودی Controlled و اعتبارسنجی شده است.
- Loading، Error و Empty State وجود دارد.
- درخواست قابل لغو است.
- پاسخ HTTP با
response.okبررسی میشود. - محتوای خارجی بدون بررسی با
innerHTMLنمایش داده نمیشود. - API Key در Frontend وجود ندارد.
- متغیرهای
VITE_حاوی Secret نیستند. - تاریخچه مکالمه محدود شده است.
- Componentهای اصلی تست رفتاری دارند.
- رابط در موبایل و Keyboard قابلاستفاده است.
- Build نهایی قبل از انتشار بررسی شده است.
- Model ID از تنظیمات Backend خوانده میشود.
- قیمت مدل در کد Hardcode نشده است.
نقشه راه یادگیری React
مرحله اول: پایه JavaScript
- Function
- Array و Object
map- Destructuring
- Module
- Promise
- Async/Await
مرحله دوم: مبانی React
- Component
- JSX
- Props
- Event
- Conditional Rendering
- List و Key
مرحله سوم: State
useState- Controlled Form
- Lifting State Up
- تغییر Immutable Array و Object
مرحله چهارم: Hookها
useEffectuseRefuseReduceruseContext- Custom Hook
مرحله پنجم: ارتباط با Backend
- Fetch API
- Loading
- Error
- AbortController
- Validation
- Authentication
مرحله ششم: معماری
- Service Layer
- Component Composition
- State Ownership
- Routing
- تست
- TypeScript
مرحله هفتم: توسعه حرفهای
- Server State
- Code Splitting
- Performance Profiling
- Accessibility
- Frameworkهایی مانند Next.js
- Deployment و Monitoring
سؤالهای متداول
React چیست؟
React یک کتابخانه JavaScript برای ساخت رابط کاربری مبتنی بر Component است. با React میتوان UI را براساس Props و State تعریف و مدیریت کرد.
آیا React زبان برنامهنویسی است؟
خیر. React کتابخانه JavaScript است. برای استفاده درست از آن باید JavaScript را یاد بگیرید.
JSX چیست؟
JSX Syntax شبیه HTML در داخل JavaScript است که برای تعریف رابط React استفاده میشود و در فرایند Build به JavaScript تبدیل میشود.
Component در React چیست؟
Component تابع یا واحد مستقلی از رابط کاربری است که میتواند Props دریافت کند و JSX برگرداند.
تفاوت Props و State چیست؟
Props از والد دریافت میشوند و فقط خواندنی هستند. State داخل Component یا Hook مدیریت میشود و با تغییر آن رابط دوباره Render میشود.
useState چیست؟
useState یک Hook برای اضافهکردن State به Function Component است و مقدار فعلی همراه با تابع بهروزرسانی را برمیگرداند.
useEffect چیست؟
useEffect برای هماهنگکردن Component با سیستمهای خارجی مانند درخواست شبکه، Timer، Event Listener و اتصالهای خارجی استفاده میشود.
آیا هر درخواست API باید داخل useEffect باشد؟
خیر. اگر درخواست در نتیجه عمل کاربر مانند ارسال فرم ایجاد میشود، معمولاً Event Handler محل مناسبتری است. Effect بیشتر برای همگامسازی براساس حضور یا تغییر Component استفاده میشود.
چرا State را نباید مستقیم تغییر دهیم؟
تغییر مستقیم Array یا Object میتواند تشخیص تغییر، Render و منطق Snapshot State را مختل کند. بهتر است مقدار جدید بسازید.
چرا key در فهرست React مهم است؟
Key به React کمک میکند هویت آیتمها را میان Renderها تشخیص دهد و تغییر، حذف یا جابهجایی آنها را درست مدیریت کند.
React بهتر است یا Vue؟
هر دو برای ساخت رابط مدرن مناسباند. انتخاب به تجربه تیم، اکوسیستم، نیاز پروژه و معماری موردنظر بستگی دارد. بهتر بودن مطلقی برای تمام پروژهها وجود ندارد.
React بهتر است یا Angular؟
React کتابخانهای انعطافپذیر برای UI است. Angular فریمورکی جامعتر با ساختار و ابزارهای داخلی بیشتر است. اندازه پروژه و ترجیح تیم در انتخاب مؤثر است.
React بهتر است یا Next.js؟
Next.js از React استفاده میکند و امکاناتی مانند Routing و Rendering سمت سرور ارائه میدهد. برای یادگیری اصول رابط، React با Vite سادهتر است؛ برای محصول کامل وب ممکن است Next.js مناسبتر باشد.
آیا React برای سئو مناسب است؟
React میتواند رابط قابلخزش ایجاد کند، اما Rendering صرفاً Client-side ممکن است برای بعضی صفحات محتوایی ایدهآل نباشد. برای صفحات عمومی و سئومحور میتوان از Rendering سمت سرور، تولید Static یا فریمورک مناسب استفاده کرد.
آیا میتوان با React اپلیکیشن هوش مصنوعی ساخت؟
بله. React برای ساخت رابط چت، داشبورد، ابزار تولید محتوا و سایر محصولات هوش مصنوعی مناسب است. اتصال به مدل باید از طریق Backend امن انجام شود.
آیا میتوان API Key درواره را در React قرار داد؟
خیر. کد React در مرورگر اجرا میشود و برای کاربر قابلمشاهده است. API Key باید فقط در Backend یا Secret Manager ذخیره شود.
Vite چیست؟
Vite ابزار توسعه و Build پروژههای وب مدرن است و Template آمادهای برای ایجاد پروژه React ارائه میکند.
آیا برای React باید Node.js نصب کنیم؟
برای اجرای ابزارهای توسعه، نصب Packageها و Build پروژه معمولاً به Node.js نیاز دارید. کد نهایی React در مرورگر اجرا میشود.
آیا React بدون TypeScript قابلاستفاده است؟
بله. React با JavaScript و TypeScript قابلاستفاده است. برای شروع میتوانید JavaScript را انتخاب کنید و پس از یادگیری مفاهیم اصلی به TypeScript بروید.
جمعبندی
React رابط کاربری را به مجموعهای از Componentهای کوچک و قابلترکیب تبدیل میکند. هر Component میتواند Props دریافت کند، State داشته باشد و براساس داده، رابط مناسب را نمایش دهد.
مهمترین مفاهیم React عبارتاند از:
- Component
- JSX
- Props
- State
- Event
- Conditional Rendering
- List و Key
useStateuseEffectuseRefuseReducer- Custom Hook
- جریان یکطرفه داده
در پروژه عملی این مقاله، یک دستیار هوش مصنوعی واقعی با React و Vite ساختیم. State گفتوگو با Reducer مدیریت شد، منطق شبکه داخل Service قرار گرفت، Custom Hook مسئول Workflow گفتوگو شد و Componentهای جداگانه رابط را نمایش دادند.
برای محافظت از کلید API، درخواست مدل مستقیماً از React ارسال نشد. Frontend فقط با Backend برنامه ارتباط برقرار میکند و Backend با استفاده از متغیر محیطی به API درواره متصل میشود.
برای دریافت API Key و شروع ساخت محصول هوش مصنوعی خود میتوانید در درواره ثبتنام کنید. برای مشاهده مدلها، انتخاب Model ID مناسب و بررسی قیمتهای بهروز نیز صفحه مدلهای درواره را ببینید.
منابع تکمیلی
مقالات مرتبط
- ساخت چتبات هوش مصنوعی با Next.js، React و API درواره
- هوش مصنوعی با Node.js؛ ساخت اپلیکیشن با Express و API درواره
- ساخت اپلیکیشن موبایل هوش مصنوعی با React Native و Expo
- هوش مصنوعی با Angular؛ آموزش اتصال به API درواره
- هوش مصنوعی با Vue.js و Nuxt؛ آموزش ساخت وباپ
- ساخت اپلیکیشن دسکتاپ هوش مصنوعی با Electron و JavaScript
- ساخت افزونه Chrome هوش مصنوعی با JavaScript و API درواره
- ساخت اپلیکیشن هوش مصنوعی با SvelteKit و API درواره
- آموزش ساخت چتبات با API هوش مصنوعی درواره
- آموزش دریافت API Key هوش مصنوعی
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.