Axios HTTP Requests Complete Guide
ডেভেলপারদের জীবনে সার্ভারের সাথে ডেটা আদান-প্রদান করা খুব সাধারণ এবং গুরুত্বপূর্ণ কাজ। এই গাইডে আমরা Axios একদম বেসিক থেকে শুরু করে instance, interceptors, JWT auth, auto refresh-token, profile system এবং একটা সম্পূর্ণ CRUD প্রজেক্ট বানিয়ে শিখবো।
Axios: সহজ ভাষায় পূর্ণাঙ্গ গাইড — জিরো থেকে প্রো
ধরুন, আপনি একটা React অ্যাপ বানাচ্ছেন যেখানে পোস্ট দেখাতে হবে, নতুন পোস্ট তৈরি করতে হবে, লাইক-কমেন্ট করতে হবে। এই সব কাজে সার্ভারের সাথে কথা বলতে হয় — ডেটা পাঠাতে হয়, ডেটা আনতে হয়। এই কাজকে বলে HTTP Request।
HTTP Request করার জন্য JavaScript-এ fetch আছে, কিন্তু Axios ব্যবহার করা বেশি সুবিধাজনক। কারণ Axios অনেক কাজ নিজেই করে দেয়: JSON পার্স করা, error সহজে ধরা, request যাওয়ার আগে বা পরে কিছু করা (Interceptors)।
এই গাইড দুটো ভাগে:
- Part 1 (সেকশন ১-৭): Axios-এর বেসিক — একটা mini json-server backend দিয়ে হাতে-কলমে request, config, response, error handling (server vs network error), CRUD (create/update/delete), instance, interceptors এবং CORS সমাধান।
- Part 2 (সেকশন ৮-৩১): রিয়েল প্রজেক্ট fb-feed — JWT Auth, Auto Refresh, Post CRUD, Profile System, Avatar Upload — সব হাতে-কলমে।
⚙️ শুরুর আগে: Backend Server রেডি করুন
Part 2-এর পুরো কাজ একটা আসল, চলমান Backend Server-এর উপর নির্ভরশীল। এটা LearnWithSumit-এর rnext রিপোজিটরির backend-server, যেটা Node.js/Express দিয়ে বানানো সম্পূর্ণ ফেসবুক-স্টাইল API।
প্রয়োজনীয় জিনিস: Git, Node.js, GitHub অ্যাকাউন্ট (কোর্স অ্যাক্সেস সহ)।
# ১. রিপোজিটরি ক্লোন করুন
git clone https://github.com/Learn-with-Sumit/rnext.git
# ২. ফোল্ডারে ঢুকুন
cd rnext
# ৩. সঠিক ব্রাঞ্চে switch করুন
git checkout 5.15
# ৪. backend ফোল্ডারে গিয়ে dependency ইনস্টল করুন
cd backend-server
npm i
# ৫. সার্ভার চালান
npm run devসবকিছু ঠিক হলে সার্ভার http://localhost:3000 পোর্টে চলবে। এই টার্মিনাল ট্যাব পুরো টিউটোরিয়াল জুড়ে খোলা রাখুন।
১. Axios কী এবং কেন ব্যবহার করবো?
Axios হলো একটি Promise-based HTTP Client।
একটা গল্প দিয়ে বোঝাই। আপনি রেস্টুরেন্টে বসে আছেন। Waiter (Axios) আপনার অর্ডার (request) নিয়ে রান্নাঘরে (server) যায়, রান্না হলে খাবার (response) নিয়ে ফেরত আসে। এই পুরো প্রক্রিয়াটাকে Axios পরিচালনা করে।
১.১ কেন fetch-এর বদলে Axios?
| বিষয় | fetch | axios |
|---|---|---|
| JSON পার্স | res.json() আলাদা call করতে হয় | অটোমেটিক করে দেয় |
| Error ধরা | শুধু network error, 4xx/5xx ধরে না | সব error সহজে ধরে |
| Interceptors | নেই | আছে — request/response উভয়েই |
| Browser + Node.js | শুধু Browser | দুটোতেই কাজ করে |
১.২ ইনস্টলেশন
npm install axios২. প্র্যাকটিসের জন্য একটা ছোট্ট Backend বানানো — json-server
Axios শিখতে একটা আসল সার্ভার লাগবে। প্রতিবার বড় backend চালু করাও ঝামেলার, তাই json-server দিয়ে কয়েক মিনিটেই একটা ফেক (fake) কিন্তু সত্যিকারের REST API বানিয়ে নিব।
মনে রাখুন: এই json-server শুধু Part 1-এর বেসিক প্র্যাকটিসের জন্য। Part 2-এর আসল fb-feed project-এ উপরে বলা
rnextbackend-ই ব্যবহার হবে।
২.১ json-server ইনস্টল করা
ভিডিওতে ব্যবহৃত স্টেবল ভার্সন 0.17.4 রাখাই ভালো, কারণ নতুন ভার্সনে মাঝে মাঝে কনফিগারেশন পাল্টে যায়:
npm install -g json-server@0.17.4২.২ db.json ফাইল বানানো
প্রজেক্ট ফোল্ডারে একটা db.json ফাইল বানান, ভেতরে কিছু ডামি ডেটা রাখুন:
{
"posts": [
{
"id": "1",
"title": "প্রথম পোস্ট",
"body": "json-server দিয়ে তৈরি প্রথম পোস্ট।"
},
{
"id": "2",
"title": "দ্বিতীয় পোস্ট",
"body": "Axios দিয়ে এই ডেটা fetch করা হবে।"
}
],
"comments": [{ "id": "1", "postId": "1", "text": "সুন্দর পোস্ট!" }]
}২.৩ সার্ভার চালু করা
json-server --watch db.json --port 8000সার্ভার চালু হলে টার্মিনালে http://localhost:8000 ঠিকানা দেখাবে। ব্রাউজারে গিয়ে ওই লিংকে চেক করুন — posts আর comments ডেটা JSON আকারে দেখাচ্ছে কিনা।
২.৪ json-server-এর Routes চেনা
json-server আপনার db.json-এর প্রতিটা key-কে automatically একটা REST resource বানিয়ে দেয়। সব HTTP method (GET, POST, PUT, PATCH, DELETE) সাপোর্ট করে, আর http বা https — দুটো দিয়েই request পাঠানো যায়:
| Method | Route | কাজ |
|---|---|---|
| GET | /posts | সব পোস্ট দেখা |
| GET | /posts/1 | নির্দিষ্ট একটা পোস্ট দেখা |
| GET | /posts/1/comments | ওই পোস্টের সব কমেন্ট দেখা |
| GET | /comments?postId=1 | query দিয়ে filter করে কমেন্ট আনা |
| POST | /posts | নতুন পোস্ট তৈরি |
| PUT | /posts/1 | পুরো পোস্ট replace করা |
| PATCH | /posts/1 | পোস্টের অংশবিশেষ আপডেট |
| DELETE | /posts/1 | পোস্ট ডিলিট করা |
গোটা এই Part 1 জুড়ে এই posts resource-টাই ব্যবহার করবো।
৩. প্রথম Request — ডেটা Read করা (GET)
ধরুন, আগে আপনার React কম্পোনেন্টে একটা লোকাল db.js ফাইল থেকে ডেটা import করে দেখাতেন। এখন সেটা বদলে আসল API (আমাদের json-server) থেকে আনতে হবে।
৩.১ State সেটআপ করা
সবার আগে ডেটা আর error রাখার জন্য state বানান:
import { useState, useEffect } from "react";
import axios from "axios";
function App() {
const [posts, setPosts] = useState([]); // শুরুতে খালি array
const [error, setError] = useState(null); // এরর মেসেজ রাখার জন্যposts-কে খালি array ([]) দিয়ে শুরু করা হচ্ছে, কারণ শুরুতে সার্ভার থেকে এখনো কোনো ডেটা আসেনি।
৩.২ useEffect-এর ভেতরে সরাসরি async কেন লেখা যায় না
useEffect-এর callback function-কে সরাসরি async বানানো যায় না। তাই ভেতরে একটা আলাদা async function বানিয়ে সেটাকে কল করতে হয়:
useEffect(() => {
const fetchPosts = async () => {
// এখানে axios call হবে
};
fetchPosts(); // ফাংশনটাকে সাথে সাথে কল করা হচ্ছে
}, []); // খালি array মানে এটা শুধু একবার, component mount হওয়ার সময় রান করবে৩.৩ axios.get() দিয়ে ডেটা আনা
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await axios.get("http://localhost:8000/posts");
// Axios সরাসরি response.data-তে রেজাল্ট দিয়ে দেয়
if (response && response.data) {
setPosts(response.data);
}
} catch (err) {
setError(err.message);
console.error("API Call Error:", err);
}
};
fetchPosts();
}, []);সাধারণ fetch ব্যবহার করলে এখানে আরেকটা .json() কল লাগত (const data = await res.json())। Axios সেটা নিজেই করে দেয় — response.data-তেই আসল ডেটা পাওয়া যায়।
৩.৪ UI-তে ডেটা এবং এরর দেখানো
return (
<div className="App">
<h1>API Request with Axios</h1>
{/* এরর থাকলে দেখাবে */}
{error && (
<div style={{ backgroundColor: "pink", padding: "10px", margin: "10px 0" }}>
Error: {error}
</div>
)}
{/* পোস্ট লিস্ট */}
<ul>
{posts.map((post) => (
<li key={post.id}>
<strong>{post.title}</strong>
</li>
))}
</ul>
</div>
);
}
export default App;সার্ভার বন্ধ রেখে test করুন — error দেখাবে। সার্ভার চালু রেখে test করুন — পোস্ট লিস্ট দেখাবে। দুই ধরনের error ঠিক কীভাবে আলাদা করে ধরতে হয়, সেটা সেকশন ৪-এ বিস্তারিত দেখবো।
৩.৫ Response Object-এর ভেতরে কী কী থাকে
const response = await axios.get("http://localhost:8000/posts");
response.data; // আসল ডেটা — সাধারণত এটাই লাগে
response.status; // HTTP status code: 200, 201, 404, 500
response.statusText; // "OK", "Created" ইত্যাদি
response.headers; // Response headers
response.config; // যে config দিয়ে request করা হয়েছিল৩.৬ Request Customize করা — Config Object
GET ছাড়াও আরো অনেক কিছু customize করা যায়:
const config = {
url: "/posts",
method: "get",
baseURL: "http://localhost:8000", // সব URL-এর আগে যুক্ত হবে
headers: {
"Content-Type": "application/json",
Authorization: "Bearer your-token-here",
},
params: { page: 1, limit: 10 }, // URL-এ query string যোগ হবে: /posts?page=1&limit=10
timeout: 10000, // কত millisecond পরে request cancel হবে
responseType: "json", // 'json', 'blob', 'text', 'arraybuffer'
};
axios(config).then((res) => console.log(res.data));৪. Error Handling — বেসিক থেকে অ্যাডভান্স
৪.১ তিন ধরনের এরর
catch (error) {
if (error.response) {
// সার্ভার response দিয়েছে, কিন্তু 4xx বা 5xx status সহ
} else if (error.request) {
// Request পাঠানো হয়েছে, কিন্তু কোনো response আসেনি
} else {
// Request বানানোর সময়ই কিছু ভুল হয়েছে
}
}৪.২ সার্ভার এরর বনাম নেটওয়ার্ক এরর
ভিডিওর গুরুত্বপূর্ণ একটা পয়েন্ট: err.response আছে কিনা চেক করেই বোঝা যায় error টা কোন ধরনের।
try {
const res = await axios.get("http://localhost:8000/posts");
setPosts(res.data);
} catch (err) {
if (err.response) {
// err.response থাকা মানে সার্ভার একটা রেসপন্স পাঠিয়েছে,
// কিন্তু সেটা ভুল status (যেমন ৪০৪) নিয়ে এসেছে
setError(`সার্ভার এরর: ${err.response.status} ${err.response.statusText}`);
} else {
// err.response নেই মানে request-ই সার্ভার পর্যন্ত পৌঁছায়নি
// (Network Error — সার্ভার বন্ধ, বা ইন্টারনেট নেই)
setError("সার্ভারের সাথে যোগাযোগ করা যাচ্ছে না।");
}
}- URL ভুল দিলে (যেমন route আছে
/posts, কিন্তু ভুল করে/postলিখলে) →err.responseপাওয়া যাবে, status হবে404। - সার্ভার বন্ধ থাকলে →
err.responseথাকবে না, এটা Network Error।
৪.৩ Dynamic Error Message বানানো
err.response.status আর err.response.statusText ব্যবহার করে ইউজারকে নির্দিষ্ট মেসেজ দেখানো যায়:
setError(
err.response
? `${err.response.status}: ${err.response.statusText}`
: "নেটওয়ার্ক এরর — সার্ভার সাড়া দিচ্ছে না।",
);৪.৪ Reusable Error Helper Function
একটা helper function রেখে দিলে পুরো প্রজেক্টের সব জায়গায় কাজে লাগবে:
function getFriendlyErrorMessage(error) {
if (!error.response) {
return "ইন্টারনেট কানেকশন বা সার্ভার সমস্যা। একটু পরে চেষ্টা করুন।";
}
const status = error.response.status;
const msg = error.response.data?.error || error.response.data?.message;
switch (status) {
case 400:
return msg || "Request-এ কিছু ভুল আছে।";
case 401:
return "লগইন করা নেই বা session শেষ।";
case 403:
return "এই কাজের অনুমতি নেই।";
case 404:
return "যা খুঁজছেন তা পাওয়া যায়নি।";
case 500:
return "সার্ভারে সমস্যা। একটু পরে চেষ্টা করুন।";
default:
return "কিছু একটা ভুল হয়েছে।";
}
}৫. ডেটা তৈরি, আপডেট ও ডিলিট করা (CRUD-এর বাকি অংশ)
এতক্ষণ শুধু Read (GET) করেছি। এবার posts Create, Update আর Delete করা শিখবো — ঠিক ভিডিওতে যেভাবে দেখানো হয়েছে।
৫.১ নতুন ডেটা তৈরি করা (Create / POST)
const handleAddPost = async (title, body) => {
try {
const newPost = {
id: Date.now().toString(), // একটা ইউনিক id বানানো
title,
body,
};
const response = await axios.post("http://localhost:8000/posts", newPost);
// সার্ভার সফলভাবে সেভ করার পর যে ডেটা ফেরত দিলো, সেটা দিয়েই local state আপডেট
setPosts((prevPosts) => [...prevPosts, response.data]);
} catch (err) {
setError(getFriendlyErrorMessage(err));
}
};handleAddPost-কে async করা হয়েছে, কারণ ভেতরে await axios.post(...) ব্যবহার হচ্ছে। POST request-এ দ্বিতীয় argument হিসেবে যে ডেটা পাঠাতে চান, সেটা যায়।
৫.২ ডেটা আপডেট করা (Update / PATCH)
const handleEditPost = async (postId, updatedFields) => {
try {
const response = await axios.patch(
`http://localhost:8000/posts/${postId}`,
updatedFields,
);
// পুরনো posts array থেকে শুধু matching id-টা replace হবে, বাকিগুলো অপরিবর্তিত থাকবে
setPosts((prevPosts) =>
prevPosts.map((post) => (post.id === postId ? response.data : post)),
);
} catch (err) {
setError(getFriendlyErrorMessage(err));
}
};PATCH বনাম PUT — পার্থক্যটা মনে রাখুন:
| Method | কাজ |
|---|---|
| PUT | পুরো object-টাই নতুন করে replace করে |
| PATCH | শুধু যেই field(গুলো) পাঠানো হয়েছে, ওইটুকুই বদলায় |
ভিডিওতে PATCH ব্যবহার করা হয়েছে, কারণ পুরো পোস্ট না পাঠিয়ে শুধু পরিবর্তিত অংশটুকু পাঠানোই যথেষ্ট।
তাহলে PUT ছাড়াই শুধু PATCH দিয়ে অ্যাপ বানানো যায়?
হ্যাঁ, পুরোপুরি যায় — এবং এটা খুবই কমন প্র্যাকটিস।
সব field পাঠালে PATCH কার্যত PUT-এর মতোই কাজ করে (পুরো resource replace হয়ে যায়)। কারণ PATCH এক টুল দিয়ে দুই কাজই (partial + full update) সামলাতে পারে, আর extra route/logic maintain করতে হয় না।
৫.৩ ডেটা মুছে ফেলা (Delete / DELETE)
const handleDeletePost = async (postId) => {
const confirmed = window.confirm("পোস্টটা সত্যিই ডিলিট করতে চান?");
if (!confirmed) return;
try {
await axios.delete(`http://localhost:8000/posts/${postId}`);
// সার্ভার থেকে ডিলিট সফল হলে, সেই id বাদে বাকি সব post রাখা হচ্ছে
setPosts((prevPosts) => prevPosts.filter((post) => post.id !== postId));
} catch (err) {
setError(getFriendlyErrorMessage(err));
}
};লক্ষ্য করুন, template literal দিয়ে URL-এর শেষে নির্দিষ্ট id জোড়া দেওয়া হচ্ছে — DELETE আর PATCH দুটোতেই একই প্যাটার্ন।
৫.৪ সংক্ষিপ্ত রেফারেন্স — সব HTTP Method এক নজরে
const res = await axios.get("/posts"); // ডেটা আনা
const res = await axios.post("/posts", data); // নতুন ডেটা তৈরি
const res = await axios.put("/posts/1", data); // পুরো ডেটা replace
const res = await axios.patch("/posts/1", data); // আংশিক আপডেট
const res = await axios.delete("/posts/1"); // ডেটা মুছে ফেলা৬. Axios Instance তৈরি করা — কোড DRY রাখা
৬.১ সমস্যা
প্রতিটা request-এ বারবার একই baseURL (http://localhost:8000) লেখা বিরক্তিকর, আর টাইপো হওয়ারও সম্ভাবনা থাকে।
৬.২ axios.create() দিয়ে সমাধান
আলাদা একটা ফাইলে (api.js) একবারই instance তৈরি করে ফেলুন:
// src/api/api.js
import axios from "axios";
const api = axios.create({
baseURL: "http://localhost:8000",
});
export default api;৬.৩ ব্যবহার করা
import api from "./api/api";
const posts = await api.get("/posts"); // baseURL বারবার লিখতে হচ্ছে না
const newPost = await api.post("/posts", { title: "নতুন পোস্ট" });মনে রাখুন: এখনই
Authorizationheader যোগ করবেন না। Token dynamically যোগ করার কাজটা Interceptor দিয়ে করবো — একদম পরের সেকশনে দেখুন।
Axios Instance নিয়ে এখনো সমস্যা থাকলে, সুমিত ভাইয়ের ৬ মিনিটের এই ভিডিওটি দেখে নিন।
[Disclimer]: এই ৬ মিনিট এর ভিডিওটি ভাইয়ের পেইড ব্যাচের ১ টি ভিডিও থেকে নেয়া। অনুমতি না পেলে আমি ডিলেট করতে বাধ্য থাকব।
৭. Interceptors ও CORS সমস্যার সমাধান
Axios-এর সবথেকে পাওয়ারফুল ফিচার হলো Interceptors — request যাওয়ার আগে বা response আসার পরে মাঝখানে বসে কোনো কাজ করার সুযোগ।
৭.১ Request Interceptor — Header অটোমেটিক যোগ করা
প্রতিটা request পাঠানোর ঠিক আগে এটা কাজ করে। এভাবে প্রতিটা call-এ আলাদা করে token বসাতে হয় না:
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});৭.২ Response Interceptor — কমন Error Handling
সার্ভার থেকে response আসার পর, কিন্তু আমাদের কোডের catch ব্লকে পৌঁছানোর আগে এটা কাজ করে:
api.interceptors.response.use(
(response) => response, // সব ঠিক থাকলে response সেভাবেই পাস হয়ে যাবে
(error) => {
console.error("API Error:", getFriendlyErrorMessage(error));
return Promise.reject(error);
},
);এর ফলে প্রতিটা ফাংশনে আলাদা করে common error handling লজিক লিখতে হয় না — একবার interceptor-এ লিখলেই পুরো অ্যাপে কাজ করবে।
৭.৩ CORS Error — কেন হয়
Custom header (যেমন Authorization) পাঠানোর সময় ব্রাউজার নিরাপত্তার কারণে অনেক সময় request block করে দেয় — একেই বলে CORS (Cross-Origin Resource Sharing) Error। Browser console-এ লাল অক্ষরে এই error দেখা যায়।
৭.৪ json-server-এ CORS সমাধান
json-server --watch db.json --port 8000 --no-cors--no-cors ফ্ল্যাগ যোগ করলে json-server ব্রাউজারকে বলে দেয় cross-origin request-এ কোনো সমস্যা নেই। আসল production backend-এ CORS ঠিক করতে হয় server-side কনফিগারেশন দিয়ে — সেটা Part 2-এ দেখবো।
এই বেসিক জিনিসগুলো জানা হয়ে গেছে — Read, Create, Update, Delete, Instance, Interceptor আর CORS পর্যন্ত। এখন আসল project-এ হাত দেওয়া যাক।
৮. প্রজেক্ট পরিচিতি: "fb-feed"
আমরা একটা মিনি সোশ্যাল মিডিয়া অ্যাপ বানাবো — নাম fb-feed। Backend হিসেবে আগে setup করা FaceHook API ব্যবহার করবো।
| Feature | Endpoint | Method |
|---|---|---|
| রেজিস্ট্রেশন | /auth/register | POST |
| লগইন | /auth/login | POST |
| Token রিফ্রেশ | /auth/refresh-token | POST |
| সব পোস্ট দেখা | /posts | GET |
| একটা পোস্ট দেখা | /posts/:id | GET |
| পোস্ট তৈরি | /posts | POST |
| পোস্ট আপডেট | /posts/:id | PATCH |
| পোস্ট ডিলিট | /posts/:id | DELETE |
| লাইক/আনলাইক | /posts/:id/like | PATCH |
| কমেন্ট করা | /posts/:id/comment | PATCH |
| কমেন্ট ডিলিট | /posts/:id/comment/:commentId | DELETE |
| প্রোফাইল দেখা | /profile/:id | GET |
| প্রোফাইল আপডেট | /profile/:id | PATCH |
| Avatar আপলোড | /profile/:id/avatar | POST |
Posts ও Profile-এর প্রায় সব endpoint-এ Bearer Token লাগে। Login না করলে 401 Unauthorized পাবে।
৯. প্রজেক্ট Setup
৯.১ Vite দিয়ে Frontend তৈরি করা
Backend চলমান রেখে নতুন টার্মিনালে:
npm create vite@latest fb-feed-frontend -- --template react
cd fb-feed-frontend
npm install
npm install axios react-router-dom৯.২ Folder Structure
src/
├── actions/
│ └── index.js ← সব action type এক জায়গায়
├── api/
│ └── api.js ← মূল Axios instance
├── context/
│ ├── AuthContext.jsx ← Login state সবখানে শেয়ার
│ └── ProfileContext.jsx ← Profile state সবখানে শেয়ার
├── hooks/
│ ├── useAuth.js ← AuthContext-এর সহজ hook
│ ├── useAxios.js ← Auth-aware API hook (interceptors সহ)
│ └── useProfile.js ← ProfileContext-এর সহজ hook
├── reducers/
│ └── profileReducer.js ← Profile state management
├── services/
│ ├── authService.js ← register, login
│ ├── postService.js ← posts-এর সব CRUD
│ └── profileService.js ← profile ও avatar
├── components/
│ ├── LoginForm.jsx
│ ├── RegisterForm.jsx
│ ├── ProtectedRoute.jsx
│ ├── PostFeed.jsx
│ ├── PostCard.jsx
│ ├── CreatePostForm.jsx
│ └── profile/
│ ├── ProfileImage.jsx
│ └── Bio.jsx
├── pages/
│ ├── LoginPage.jsx
│ ├── RegisterPage.jsx
│ ├── FeedPage.jsx
│ └── ProfilePage.jsx
└── App.jsx৯.৩ .env ফাইল
Project-এর root-এ:
VITE_API_BASE_URL=http://localhost:3000৯.৪ Axios Instance তৈরি
// src/api/api.js
import axios from "axios";
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
});
export default api;১০. JWT কী? কেন দরকার?
Login করার পর server দুটো জিনিস দেয়:
{
"user": {
"id": "a9f2813e-a0a3-4ae0-80a1-570f3d5fbd23",
"firstName": "Mojnu",
"lastName": "Miah",
"email": "mojnu@example.com",
"avatar": null
},
"token": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}Access Token — আপনার পরিচয়পত্র। প্রতিটা protected API call-এ এটা Header-এ পাঠাতে হয়। নিরাপত্তার জন্য মেয়াদ কম — সাধারণত ১৫–৩০ মিনিট।
Refresh Token — পরিচয়পত্র নবায়নের ফর্ম। মেয়াদ অনেক বেশি — কয়েক দিন থেকে সপ্তাহ। Access Token expire হলে এটা দিয়ে নতুন Access Token চাওয়া যায়, user-কে আবার login না করিয়েই।
আমাদের কাজ:
- Login-এর পর দুটো token state-এ save করা।
- প্রতিটা protected request-এ Access Token পাঠানো।
401পেলে চুপচাপ Refresh Token দিয়ে নতুন Access Token নেওয়া।- Refresh Token-ও expire হলে logout করে login page-এ পাঠানো।
এই পুরো flow টা Interceptor দিয়ে অটোমেট করবো।
১১. Actions — State পরিবর্তনের নাম এক জায়গায়
// src/actions/index.js
export const actions = {
profile: {
PROFILE_FETCHING: "PROFILE_FETCHING",
PROFILE_FETCHED: "PROFILE_FETCHED",
PROFILE_FETCH_ERROR: "PROFILE_FETCH_ERROR",
PROFILE_UPDATED: "PROFILE_UPDATED",
IMAGE_UPDATED: "IMAGE_UPDATED",
},
};এভাবে রাখলে IDE autocomplete দেবে, typo হলে সাথে সাথে ধরা পড়বে।
১২. AuthContext — Login State সবখানে শেয়ার করা
// src/context/AuthContext.jsx
import { createContext, useState } from "react";
const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState(null);
// auth object: { user, authToken, refreshToken }
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
);
};
export default AuthContext;// src/hooks/useAuth.js
import { useContext } from "react";
import AuthContext from "../context/AuthContext";
const useAuth = () => {
return useContext(AuthContext);
};
export default useAuth;১৩. useAxios Custom Hook — Auth-aware API Client
এটাই সবচেয়ে গুরুত্বপূর্ণ অংশ। এই hook:
- প্রতিটা request-এ অটোমেটিক
Authorizationheader যোগ করে। 401পেলে Refresh Token দিয়ে নতুন Access Token নিয়ে আগের request আবার পাঠায়।- Component unmount হলে interceptor সরিয়ে দেয় (memory leak বন্ধ করা)।
// src/hooks/useAxios.js
import { useEffect } from "react";
import api from "../api/api";
import useAuth from "./useAuth";
const useAxios = () => {
const { auth, setAuth } = useAuth();
useEffect(() => {
// Request Interceptor — প্রতিটা request-এর আগে চলে
const requestIntercept = api.interceptors.request.use(
(config) => {
const token = auth?.authToken;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// FormData (ছবি আপলোড) হলে Content-Type নিজে সেট করবেন না
if (config.data instanceof FormData) {
delete config.headers["Content-Type"];
} else {
config.headers["Content-Type"] = "application/json";
}
return config;
},
(error) => Promise.reject(error),
);
// Response Interceptor — প্রতিটা response আসার পরে চলে
const responseIntercept = api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// originalRequest.alreadyRetried এখনো false/undefined মানে —
// এই request-এর জন্য আমরা এখনো একবারও refresh চেষ্টা করিনি।
const isFirstFailure =
error.response?.status === 401 && !originalRequest.alreadyRetried;
if (isFirstFailure) {
// মার্ক করে দিলাম: "এই request একবার retry হচ্ছে"
// এর ফলে refresh token নিজেও যদি fail করে, তখন এই if-এ
// আর দ্বিতীয়বার ঢুকবে না — infinite loop বন্ধ হয়ে যায়
originalRequest.alreadyRetried = true;
try {
const { data } = await api.post("/auth/refresh-token", {
refreshToken: auth?.refreshToken,
});
setAuth((prev) => ({ ...prev, authToken: data.token }));
originalRequest.headers.Authorization = `Bearer ${data.token}`;
return api(originalRequest);
} catch (refreshError) {
// Refresh token-ও কাজ করলো না — session শেষ, logout
setAuth(null);
window.location.href = "/login";
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
},
);
return () => {
api.interceptors.request.eject(requestIntercept);
api.interceptors.response.eject(responseIntercept);
};
}, [auth]);
return { api };
};
export default useAxios;এই Hook (useAxios) কী কাজ করে?
useAxios হলো একটি custom hook, যা Axios-এর request ও response interceptor ব্যবহার করে সব API request একটি নির্দিষ্ট নিয়মে পরিচালনা করে।
১৩.১ Request পাঠানোর আগে
- User login করা থাকলে
Authorizationheader-এ Bearer Token যুক্ত করে। - যদি
FormData(যেমন: ছবি আপলোড) পাঠানো হয়, তাহলেContent-Typeheader মুছে দেয়। কারণ browser নিজেই সঠিকContent-Typeসেট করে। জোর করে সেট করলে file upload সমস্যা হতে পারে।
১৩.২ Response আসার পরে
- Token expire হয়ে গেলে (401 Unauthorized),
refreshTokenব্যবহার করে নতুন token আনে। setAuth()দিয়ে নতুন token state-এ সংরক্ষণ করে।- এরপর নতুন token দিয়ে আগের request আবার (retry) পাঠায়।
- যদি refresh token দিয়েও নতুন token না পাওয়া যায়, তাহলে user-কে logout করে
/loginপেজে পাঠিয়ে দেয়।
Retry কী?
Retry অর্থ হলো, একটি request আরেকবার চেষ্টা করা।
এখানে alreadyRetried ব্যবহার করা হয়েছে, যাতে একই request শুধুমাত্র একবারই retry হয়। এটি না থাকলে 401 error বারবার এসে infinite loop তৈরি হতে পারত।
Cleanup
Component unmount হলে request ও response interceptor দুটিই remove (eject) করে দেয়। এতে একই interceptor বারবার যুক্ত হয় না এবং memory leak-এর সম্ভাবনাও থাকে না।
সংক্ষেপে
এই useAxios hook এক জায়গা থেকেই Token যোগ করা, Token Refresh করা, Request Retry করা এবং Logout—সবকিছু স্বয়ংক্রিয়ভাবে পরিচালনা করে।
- Public API (Login, Register ইত্যাদি) →
import { api } from "../api" ব্যবহার করা যেতে পারে। - Protected API (Token লাগে) →
const { api } = useAxios()ব্যবহার করো।
আসলে const { api } = useAxios() সব API call-এর জন্যই ব্যবহার করতে পারো, এমনকি Login/Register-এর জন্যও।
- ✅
const { api } = useAxios()সব API call-এর জন্য ব্যবহার করা যাবে (Public + Protected)। - ✅ এটি ব্যবহার করলে একই Axios instance থাকবে এবং interceptor-এর সব সুবিধা (Authorization, Refresh Token, 401 Retry) স্বয়ংক্রিয়ভাবে কাজ করবে।
- ✅ Codebase consistent ও maintainable থাকবে।
- ❌
import { api } from "../api"আলাদাভাবে ব্যবহার করার প্রয়োজন নেই, যদিuseAxios()সব জায়গায় ব্যবহার করো।
Recommendation: পুরো project-এ একটি pattern অনুসরণ করো—
const { api } = useAxios();এবং সব API request এই api দিয়েই করো। এতে ভবিষ্যতে maintenance অনেক সহজ হবে।
১৪. Auth Service — Login ও Register
Service Layer ব্যবহার করা ভালো, তবে ছোট project-এ এটি বাধ্যতামূলক নয়।
আমার Recommendation
✅ ছোট Project (5–10 API)
Service layer না থাকলেও সমস্যা নেই।
const api = useAxios();
await api.get("/posts");
await api.post("/posts");✅ Medium/Large Project (Recommended)
Service layer ব্যবহার করো।
// authService.js
export const loginUser = (api, email, password) =>
api.post("/auth/login", { email, password });
export const registerUser = (api, payload) =>
api.post("/auth/register", payload);// Component
const { api } = useAxios();
await loginUser(api, email, password);কেন Service Layer ভালো?
- ✅ সব API endpoint এক জায়গায় থাকে।
- ✅ Component পরিষ্কার (clean) থাকে।
- ✅ Endpoint পরিবর্তন হলে এক জায়গায় পরিবর্তন করলেই হয়।
- ✅ একই API অনেক Component-এ reuse করা যায়।
- ✅ বড় project maintain করা সহজ হয়।
আমার ক্ষেত্রে কী করতাম?
Architecture:
Component
↓
useAxios()
↓
Service Layer
↓
APIএটি clean, scalable এবং professional pattern।
এই function গুলো শুধু API call করে। State management বা navigation এখানে করা হয়নি। এতে service file clean ও testable থাকে।
১৫. Login Form
// src/components/LoginForm.jsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import useAuth from "../hooks/useAuth";
import { loginUser } from "../services/authService";
export default function LoginForm() {
const { setAuth } = useAuth();
const navigate = useNavigate();
const [form, setForm] = useState({ email: "", password: "" });
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
setError("");
setLoading(true);
try {
const response = await loginUser(form.email, form.password);
if (response.status === 200) {
// Server থেকে আসা response:
// { user: {...}, token: { token: "...", refreshToken: "..." } }
const { user, token } = response.data;
const { token: authToken, refreshToken } = token;
setAuth({ user, authToken, refreshToken });
navigate("/");
}
} catch (err) {
setError(
err.response?.data?.error || "লগইন ব্যর্থ হয়েছে। আবার চেষ্টা করুন।",
);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
type="email"
placeholder="ইমেইল"
value={form.email}
onChange={handleChange}
required
/>
<input
name="password"
type="password"
placeholder="পাসওয়ার্ড"
value={form.password}
onChange={handleChange}
required
/>
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit" disabled={loading}>
{loading ? "লগইন হচ্ছে..." : "লগইন করুন"}
</button>
</form>
);
}১৬. Register Form
Register আর Login-এর pattern একই — শুধু field বেশি, আর সফল হলে সরাসরি Login করিয়ে দেওয়া হচ্ছে:
// src/components/RegisterForm.jsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import useAuth from "../hooks/useAuth";
import { registerUser, loginUser } from "../services/authService";
export default function RegisterForm() {
const { setAuth } = useAuth();
const navigate = useNavigate();
const [form, setForm] = useState({
firstName: "",
lastName: "",
email: "",
password: "",
});
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
setError("");
setLoading(true);
try {
// প্রথমে registration করুন
await registerUser(form);
// সফল হলে সাথে সাথে login করিয়ে দিন
const loginRes = await loginUser(form.email, form.password);
const { user, token } = loginRes.data;
const { token: authToken, refreshToken } = token;
setAuth({ user, authToken, refreshToken });
navigate("/");
} catch (err) {
setError(
err.response?.data?.error ||
"রেজিস্ট্রেশন ব্যর্থ হয়েছে। আবার চেষ্টা করুন।",
);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
name="firstName"
placeholder="প্রথম নাম"
value={form.firstName}
onChange={handleChange}
required
/>
<input
name="lastName"
placeholder="শেষ নাম"
value={form.lastName}
onChange={handleChange}
required
/>
<input
name="email"
type="email"
placeholder="ইমেইল"
value={form.email}
onChange={handleChange}
required
/>
<input
name="password"
type="password"
placeholder="পাসওয়ার্ড"
value={form.password}
onChange={handleChange}
required
/>
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit" disabled={loading}>
{loading ? "একাউন্ট তৈরি হচ্ছে..." : "রেজিস্ট্রেশন করুন"}
</button>
</form>
);
}১৭. ProtectedRoute — অননুমোদিত প্রবেশ আটকানো
Login না করে কেউ যেন সরাসরি Feed page-এ না যেতে পারে:
// src/components/ProtectedRoute.jsx
import { Navigate } from "react-router-dom";
import useAuth from "../hooks/useAuth";
export default function ProtectedRoute({ children }) {
const { auth } = useAuth();
if (!auth) {
return <Navigate to="/login" replace />;
}
return children;
}১৮. Post Service — সব CRUD Function এক জায়গায়
// src/services/postService.js
import api from "../api/api";
// সব পোস্ট আনা
export const getPosts = () => api.get("/posts");
// একটা পোস্ট আনা
export const getPost = (id) => api.get(`/posts/${id}`);
// নতুন পোস্ট তৈরি (টেক্সট + ঐচ্ছিক ছবি)
export const createPost = ({ content, image }, onUploadProgress) => {
const formData = new FormData();
formData.append("postType", image ? "image" : "text");
formData.append("content", content);
if (image) {
formData.append("image", image);
}
return api.post("/posts", formData, {
onUploadProgress: (progressEvent) => {
if (!onUploadProgress) return;
const percent = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 1),
);
onUploadProgress(percent);
},
});
};
// পোস্ট আপডেট
export const updatePost = (id, { content }) => {
const formData = new FormData();
formData.append("content", content);
return api.patch(`/posts/${id}`, formData);
};
// পোস্ট ডিলিট
export const deletePost = (id) => api.delete(`/posts/${id}`);
// লাইক বা আনলাইক
export const toggleLike = (id) => api.patch(`/posts/${id}/like`);
// কমেন্ট করা
export const addComment = (postId, comment) =>
api.patch(`/posts/${postId}/comment`, { comment });
// কমেন্ট ডিলিট
export const deleteComment = (postId, commentId) =>
api.delete(`/posts/${postId}/comment/${commentId}`);কোডে একটা গুরুত্বপূর্ণ বিষয়: এই
apiinstance-এ সরাসরি token নেই। Token তোuseAxioshook-এ interceptor-এ যোগ হয়। কিন্তু কীভাবে কাজ করে?JavaScript module singleton।
useAxioshook যেapiinstance-এ interceptor attach করে, সেটাই এই service file-এরapi— কারণ দুটোই একইsrc/api/api.jsথেকে import। তাইuseAxiosযখন interceptor দেয়, সেটা পুরো app জুড়ে সেইapi-তে কাজ করে।
১৯. Post Feed — সব পোস্ট দেখানো
// src/components/PostFeed.jsx
import { useEffect, useState } from "react";
import useAxios from "../hooks/useAxios";
import { getPosts } from "../services/postService";
import PostCard from "./PostCard";
export default function PostFeed() {
const { api } = useAxios(); // interceptor activate করতে call করতে হবে
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
let isMounted = true;
const fetchPosts = async () => {
try {
const res = await getPosts();
if (isMounted) setPosts(res.data);
} catch (err) {
if (isMounted) setError("পোস্ট লোড করা যায়নি। পরে আবার চেষ্টা করুন।");
} finally {
if (isMounted) setLoading(false);
}
};
fetchPosts();
return () => {
isMounted = false;
};
}, []);
if (loading) return <p>পোস্ট লোড হচ্ছে...</p>;
if (error) return <p style={{ color: "red" }}>{error}</p>;
if (posts.length === 0) return <p>এখনো কোনো পোস্ট নেই।</p>;
return (
<div>
{posts.map((post) => (
<PostCard key={post.id} post={post} onListChange={setPosts} />
))}
</div>
);
}isMounted flag ব্যবহার করার কারণ: component unmount হওয়ার পরেও কখনো কখনো async API call শেষ হয় এবং setState call করার চেষ্টা করে। React এতে warning দেয়। isMounted দিয়ে এটা ঠেকানো হয়।
২০. Create Post Form — ছবি আপলোড + Progress Bar
// src/components/CreatePostForm.jsx
import { useState } from "react";
import useAxios from "../hooks/useAxios";
import { createPost } from "../services/postService";
export default function CreatePostForm({ onCreated }) {
const { api } = useAxios();
const [content, setContent] = useState("");
const [image, setImage] = useState(null);
const [progress, setProgress] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e) => {
e.preventDefault();
if (!content.trim()) return;
setLoading(true);
setProgress(0);
setError("");
try {
const res = await createPost({ content, image }, setProgress);
onCreated?.(res.data);
setContent("");
setImage(null);
} catch (err) {
setError(
err.response?.data?.error || "পোস্ট তৈরি হয়নি। আবার চেষ্টা করুন।",
);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="আপনি কী ভাবছেন?"
required
/>
<input
type="file"
accept="image/*"
onChange={(e) => setImage(e.target.files?.[0] ?? null)}
/>
{loading && (
<div>
<progress value={progress} max="100" />
<span>{progress}%</span>
</div>
)}
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit" disabled={loading}>
{loading ? `আপলোড হচ্ছে... ${progress}%` : "পোস্ট করুন"}
</button>
</form>
);
}২১. PostCard — Edit, Delete, Like, Comment
// src/components/PostCard.jsx
import { useState } from "react";
import useAxios from "../hooks/useAxios";
import {
updatePost,
deletePost,
toggleLike,
addComment,
deleteComment,
} from "../services/postService";
export default function PostCard({ post, onListChange }) {
const { api } = useAxios();
const [editing, setEditing] = useState(false);
const [content, setContent] = useState(post.content ?? post.body);
const [commentText, setCommentText] = useState("");
const removeFromList = (id) =>
onListChange((prev) => prev.filter((p) => p.id !== id));
// পুরো list re-fetch না করে শুধু একটা পোস্ট update করুন
const replaceInList = (updated) =>
onListChange((prev) =>
prev.map((p) => (p.id === updated.id ? updated : p)),
);
const handleUpdate = async () => {
try {
const res = await updatePost(post.id, { content });
replaceInList(res.data);
setEditing(false);
} catch (err) {
console.error("আপডেট ব্যর্থ:", err.message);
}
};
const handleDelete = async () => {
if (!confirm("পোস্টটা কি সত্যিই মুছবে?")) return;
try {
await deletePost(post.id);
removeFromList(post.id);
} catch (err) {
console.error("ডিলিট ব্যর্থ:", err.message);
}
};
const handleLike = async () => {
try {
const res = await toggleLike(post.id);
replaceInList(res.data);
} catch (err) {
console.error("লাইক ব্যর্থ:", err.message);
}
};
const handleComment = async (e) => {
e.preventDefault();
if (!commentText.trim()) return;
try {
const res = await addComment(post.id, commentText);
replaceInList(res.data);
setCommentText("");
} catch (err) {
console.error("কমেন্ট ব্যর্থ:", err.message);
}
};
const handleDeleteComment = async (commentId) => {
try {
const res = await deleteComment(post.id, commentId);
replaceInList(res.data);
} catch (err) {
console.error("কমেন্ট ডিলিট ব্যর্থ:", err.message);
}
};
return (
<div className="post-card">
{editing ? (
<div>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<button onClick={handleUpdate}>সেভ করুন</button>
<button onClick={() => setEditing(false)}>বাতিল</button>
</div>
) : (
<div>
<p>{post.content ?? post.body}</p>
{post.image && <img src={post.image} alt="পোস্টের ছবি" />}
<button onClick={() => setEditing(true)}>এডিট</button>
<button onClick={handleDelete}>ডিলিট</button>
</div>
)}
<button onClick={handleLike}>👍 লাইক ({post.likes?.length ?? 0})</button>
<ul>
{post.comments?.map((c) => (
<li key={c.id}>
<strong>{c.author?.firstName}:</strong> {c.comment}{" "}
<button onClick={() => handleDeleteComment(c.id)}>✕</button>
</li>
))}
</ul>
<form onSubmit={handleComment}>
<input
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder="একটা কমেন্ট লিখুন..."
/>
<button type="submit">কমেন্ট করুন</button>
</form>
</div>
);
}একটা কৌশল: Like, comment, বা update-এর পর পুরো post list আবার fetch করা হচ্ছে না। শুধু সেই একটা post-কে server থেকে আসা নতুন data দিয়ে replace করা হচ্ছে। এতে app অনেক দ্রুত মনে হয়।
২২. Profile System — Reducer + Context Pattern
Profile page-এ অনেক ধরনের action: data fetch, ছবি বদলানো, bio update। এত কিছু component-এ রাখলে component বিশাল হয়ে যাবে। এর সমাধান — Reducer pattern।
২২.১ Profile Reducer
// src/reducers/profileReducer.js
import { actions } from "../actions";
export const initialState = {
user: null,
posts: [],
loading: false,
error: null,
};
const profileReducer = (state, action) => {
switch (action.type) {
case actions.profile.PROFILE_FETCHING:
return { ...state, loading: true, error: null };
case actions.profile.PROFILE_FETCHED:
return {
...state,
user: action.data.user,
posts: action.data.posts,
loading: false,
error: null,
};
case actions.profile.PROFILE_FETCH_ERROR:
return { ...state, loading: false, error: action.error };
case actions.profile.PROFILE_UPDATED:
return { ...state, user: action.data, loading: false };
case actions.profile.IMAGE_UPDATED:
// শুধু avatar বদলান, বাকি user data ঠিক রাখুন
return {
...state,
user: { ...state.user, avatar: action.data.avatar },
loading: false,
};
default:
return state;
}
};
export default profileReducer;২২.২ ProfileContext ও ProfileProvider
// src/context/ProfileContext.jsx
import { createContext, useReducer } from "react";
import profileReducer, { initialState } from "../reducers/profileReducer";
const ProfileContext = createContext(null);
export const ProfileProvider = ({ children }) => {
const [state, dispatch] = useReducer(profileReducer, initialState);
return (
<ProfileContext.Provider value={{ state, dispatch }}>
{children}
</ProfileContext.Provider>
);
};
export default ProfileContext;// src/hooks/useProfile.js
import { useContext } from "react";
import ProfileContext from "../context/ProfileContext";
const useProfile = () => {
return useContext(ProfileContext);
};
export default useProfile;২৩. Profile Page — Data Fetch করা
// src/pages/ProfilePage.jsx
import { useEffect } from "react";
import { actions } from "../actions";
import useAuth from "../hooks/useAuth";
import useAxios from "../hooks/useAxios";
import useProfile from "../hooks/useProfile";
import ProfileImage from "../components/profile/ProfileImage";
import Bio from "../components/profile/Bio";
const ProfilePage = () => {
const { auth } = useAuth();
const { api } = useAxios();
const { state, dispatch } = useProfile();
useEffect(() => {
if (!auth?.user?.id) return;
const fetchProfile = async () => {
dispatch({ type: actions.profile.PROFILE_FETCHING });
try {
const response = await api.get(`/profile/${auth.user.id}`);
dispatch({
type: actions.profile.PROFILE_FETCHED,
data: response.data,
});
} catch (error) {
dispatch({
type: actions.profile.PROFILE_FETCH_ERROR,
error: error.response?.data?.message || error.message,
});
}
};
fetchProfile();
}, [auth?.user?.id]);
if (state.loading) return <p>প্রোফাইল লোড হচ্ছে...</p>;
if (state.error) return <p style={{ color: "red" }}>এরর: {state.error}</p>;
if (!state.user) return null;
return (
<div>
<ProfileImage />
<h1>
{state.user.firstName} {state.user.lastName}
</h1>
<Bio />
<p>{state.posts?.length} টা পোস্ট</p>
</div>
);
};
export default ProfilePage;২৪. ProfileImage — ছবি আপলোড useRef দিয়ে
useRef ব্যবহার করে লুকানো file input-এ programmatically click করা হচ্ছে — এতে custom styled button দেখানো যায়:
// src/components/profile/ProfileImage.jsx
import { useRef } from "react";
import { actions } from "../../actions";
import useAuth from "../../hooks/useAuth";
import useAxios from "../../hooks/useAxios";
import useProfile from "../../hooks/useProfile";
const ProfileImage = () => {
const { auth } = useAuth();
const { api } = useAxios();
const { state, dispatch } = useProfile();
const fileInputRef = useRef(null);
// Edit button click করলে লুকানো file input খুলবে
const handleEditClick = (e) => {
e.preventDefault();
fileInputRef.current.click();
};
// ফাইল select হলে সাথে সাথে upload করবে
const handleFileChange = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
try {
const formData = new FormData();
formData.append("avatar", file);
// Content-Type header useAxios interceptor নিজেই handle করবে
const response = await api.post(
`/profile/${auth.user.id}/avatar`,
formData,
);
if (response.status === 200) {
dispatch({
type: actions.profile.IMAGE_UPDATED,
data: response.data,
});
}
} catch (err) {
dispatch({
type: actions.profile.PROFILE_FETCH_ERROR,
error: err.message,
});
}
};
// Server থেকে avatar path আসে: "uploads/avatar/filename.jpg"
// পুরো URL বানাতে base URL যোগ করতে হয়
const avatarUrl = state.user?.avatar
? `${import.meta.env.VITE_API_BASE_URL}/${state.user.avatar}`
: "https://via.placeholder.com/180";
return (
<div style={{ position: "relative", width: 180, height: 180 }}>
<img
src={avatarUrl}
alt={state.user?.firstName}
style={{
width: "100%",
height: "100%",
borderRadius: "50%",
objectFit: "cover",
}}
/>
<button
onClick={handleEditClick}
style={{
position: "absolute",
bottom: 8,
right: 8,
background: "rgba(0,0,0,0.6)",
border: "none",
borderRadius: "50%",
cursor: "pointer",
padding: 6,
}}
title="ছবি বদলান"
>
✏️
</button>
{/* লুকানো file input */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileChange}
style={{ display: "none" }}
/>
</div>
);
};
export default ProfileImage;কেন useRef দিয়ে click করছি?
Default file input কুৎসিত দেখায়। নিজের design করা button দেখাই, কিন্তু আসল file-select কাজটা hidden input করে। useRef দিয়ে সেই hidden input-এর DOM element ধরে .click() করলে file picker খোলে।
২৫. Bio — Text Update editMode দিয়ে
// src/components/profile/Bio.jsx
import { useState } from "react";
import { actions } from "../../actions";
import useAuth from "../../hooks/useAuth";
import useAxios from "../../hooks/useAxios";
import useProfile from "../../hooks/useProfile";
const Bio = () => {
const { auth } = useAuth();
const { api } = useAxios();
const { state, dispatch } = useProfile();
const [editMode, setEditMode] = useState(false);
const [bio, setBio] = useState(state.user?.bio ?? "");
const handleSave = async () => {
dispatch({ type: actions.profile.PROFILE_FETCHING });
try {
// ⚠️ Backend quirk: profile update-এ field নাম "firstname" (lowercase n)
// কিন্তু registration-এ "firstName" (capital N) — backend inconsistency।
// সবসময় Postman দিয়ে confirm করুন।
const response = await api.patch(`/profile/${auth.user.id}`, { bio });
dispatch({
type: actions.profile.PROFILE_UPDATED,
data: response.data,
});
setEditMode(false);
} catch (error) {
dispatch({
type: actions.profile.PROFILE_FETCH_ERROR,
error: error.message,
});
}
};
return (
<div>
{editMode ? (
<div>
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
rows={4}
placeholder="আপনার সম্পর্কে কিছু লিখুন..."
/>
<button onClick={handleSave}>সেভ করুন</button>
<button onClick={() => setEditMode(false)}>বাতিল</button>
</div>
) : (
<div>
<p>{state.user?.bio || "এখনো কোনো bio যোগ করা হয়নি।"}</p>
<button onClick={() => setEditMode(true)}>✏️ Bio এডিট করুন</button>
</div>
)}
</div>
);
};
export default Bio;২৬. Profile Service
// src/services/profileService.js
import api from "../api/api";
export const getProfile = (id) => api.get(`/profile/${id}`);
// ⚠️ backend quirk: field নাম "firstname" (lowercase n)
export const updateProfile = (id, { firstname, lastName, bio }) =>
api.patch(`/profile/${id}`, { firstname, lastName, bio });
export const uploadAvatar = (id, file, onUploadProgress) => {
const formData = new FormData();
formData.append("avatar", file);
return api.post(`/profile/${id}/avatar`, formData, {
onUploadProgress: (progressEvent) => {
if (!onUploadProgress) return;
const percent = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 1),
);
onUploadProgress(percent);
},
});
};২৭. একসাথে একাধিক Request — Promise.all
কোনো page-এ একসাথে profile আর posts দরকার হলে, একটার পর একটা না এনে একসাথে আনুন:
// ❌ ধীর: sequential (দুটোর সময় যোগ হয়)
const profileRes = await api.get(`/profile/${userId}`);
const postsRes = await api.get("/posts");
// ✅ দ্রুত: parallel (যেটা বেশি সময় নেয় শুধু সেটার সময় লাগে)
const [profileRes, postsRes] = await Promise.all([
api.get(`/profile/${userId}`),
api.get("/posts"),
]);
const profile = profileRes.data;
const posts = postsRes.data;দুটো request যদি একে অপরের উপর নির্ভর না করে, সবসময় Promise.all ব্যবহার করুন।
২৮. Request Cancel করা — AbortController
Search box-এ প্রতিটা keystroke-এ API call যায়। আগেরটার response যদি নতুনটার পরে আসে, পুরনো data দিয়ে নতুন result overwrite হয়ে যাবে। এটা ঠেকাতে আগের request cancel করুন:
import { useEffect, useState } from "react";
import api from "../api/api";
function useSearchPosts(query) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query.trim()) {
setResults([]);
return;
}
const controller = new AbortController();
setLoading(true);
api
.get("/posts", {
params: { search: query },
signal: controller.signal,
})
.then((res) => {
setResults(res.data);
setLoading(false);
})
.catch((err) => {
// "CanceledError" মানে আমরাই cancel করেছি — এটা real error নয়
if (err.name !== "CanceledError") {
console.error("সার্চ এরর:", err.message);
setLoading(false);
}
});
return () => {
controller.abort(); // পরের keystroke-এ আগের request cancel হবে
};
}, [query]);
return { results, loading };
}২৯. সম্পূর্ণ App — React Router দিয়ে সব জোড়া লাগানো
২৯.১ Feed Page
// src/pages/FeedPage.jsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import useAuth from "../hooks/useAuth";
import CreatePostForm from "../components/CreatePostForm";
import PostFeed from "../components/PostFeed";
export default function FeedPage() {
const { auth, setAuth } = useAuth();
const navigate = useNavigate();
const [refreshKey, setRefreshKey] = useState(0);
const handleLogout = () => {
setAuth(null);
navigate("/login");
};
return (
<div>
<header>
<h2>স্বাগতম, {auth?.user?.firstName}!</h2>
<button onClick={handleLogout}>লগআউট</button>
</header>
<CreatePostForm onCreated={() => setRefreshKey((k) => k + 1)} />
{/* key বদলালে PostFeed নতুন করে mount হয়, useEffect আবার চলে */}
<PostFeed key={refreshKey} />
</div>
);
}২৯.২ Login ও Register Page
// src/pages/LoginPage.jsx
import { Link } from "react-router-dom";
import LoginForm from "../components/LoginForm";
export default function LoginPage() {
return (
<div>
<h2>লগইন করুন</h2>
<LoginForm />
<p>
একাউন্ট নেই? <Link to="/register">রেজিস্ট্রেশন করুন</Link>
</p>
</div>
);
}// src/pages/RegisterPage.jsx
import { Link } from "react-router-dom";
import RegisterForm from "../components/RegisterForm";
export default function RegisterPage() {
return (
<div>
<h2>নতুন একাউন্ট খুলুন</h2>
<RegisterForm />
<p>
আগে থেকে একাউন্ট আছে? <Link to="/login">লগইন করুন</Link>
</p>
</div>
);
}২৯.৩ App.jsx — সব Route এক জায়গায়
// src/App.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./context/AuthContext";
import { ProfileProvider } from "./context/ProfileContext";
import ProtectedRoute from "./components/ProtectedRoute";
import LoginPage from "./pages/LoginPage";
import RegisterPage from "./pages/RegisterPage";
import FeedPage from "./pages/FeedPage";
import ProfilePage from "./pages/ProfilePage";
export default function App() {
return (
// AuthProvider সবার বাইরে — সব component-এ auth দরকার
<AuthProvider>
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
{/* Protected routes — login করা লাগবে */}
<Route
path="/"
element={
<ProtectedRoute>
<FeedPage />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
{/* ProfileProvider শুধু ProfilePage-এর জন্য */}
<ProfileProvider>
<ProfilePage />
</ProfileProvider>
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</AuthProvider>
);
}২৯.৪ App চালান ও Test করুন
npm run devhttp://localhost:5173 খুলে এই flow test করুন:
/register— নতুন account তৈরি করুন। সফল হলে auto login হয়ে Feed page-এ যাবে।- Feed page-এ একটা post তৈরি করুন। Network tab-এ দেখুন
Authorization: Bearer ...header যাচ্ছে। - Like, comment, edit, delete test করুন।
/profilepage-এ গিয়ে avatar upload করুন এবং bio update করুন।
৩০. সেরা অনুশীলন — Best Practices
Architecture:
- ✅ একটাই central
apiinstance — প্রতিটা file-এ আলাদাaxios.create()না। - ✅ Service layer আলাদা রাখুন — component-এ সরাসরি
axios.get()না। - ✅ Auth-aware requests-এর জন্য
useAxioshook ব্যবহার করুন। - ✅ Complex state-এ
useReducerব্যবহার করুন (যেমন profile system)।
Interceptors:
- ✅
Authorizationheader interceptor দিয়ে যোগ করুন, manually প্রতিটা call-এ না। - ✅ Component unmount হলে interceptor eject করুন (memory leak ঠেকাতে)।
- ✅
_retryflag ব্যবহার করুন — নাহলে infinite refresh loop হতে পারে।
FormData:
- ✅ ছবি আপলোডের সময় নিজে
Content-Type: multipart/form-dataসেট করবেন না। - ✅ Browser নিজেই সঠিক
boundaryসহ header বসায় — জোর করলে upload ভাঙবে।
Error Handling:
- ✅
error.response,error.request,error.message— তিন case আলাদা করে handle করুন। - ✅ User-friendly error message দেখান, raw API error না।
Performance:
- ✅ স্বাধীন request গুলো
Promise.allদিয়ে parallel করুন। - ✅ Search বা frequent call-এ
AbortControllerব্যবহার করুন।
৩১. Security নোট — Token কোথায় রাখবো?
| পদ্ধতি | সুবিধা | অসুবিধা |
|---|---|---|
| React State (memory) | XSS-safe, simple | Page refresh-এ হারায় |
| localStorage | Persist করে | XSS attack-এ চুরি হতে পারে |
| httpOnly Cookie | XSS-safe, persist করে | CSRF protect করতে হয়, backend cooperation লাগে |
সুপারিশ: FaceHook API যেহেতু body-তে token পাঠায়, React state বা localStorage উভয়ই practical। Production-এ sensitive token (বিশেষত Refresh Token) httpOnly cookie-তে রাখা সবচেয়ে নিরাপদ। সেক্ষেত্রে axios.create({ withCredentials: true }) যোগ করলেই হবে।
উপসংহার
এই গাইডে আপনি শিখলেন:
- Basics: সব HTTP method, configuration, response structure, error handling।
- Instance: একটা central
apiদিয়ে DRY code। - useAxios Hook: Auth-aware API client, interceptor with cleanup।
- JWT Flow: Access Token → 401 → Refresh → Retry — সম্পূর্ণ automation।
- Auth System: Register, Login, Protected Route, AuthContext।
- Post CRUD: Create, Read, Update, Delete, Like, Comment — সবকিছু।
- Profile System: Reducer + Context pattern, avatar upload with
useRef, bio update। - Advanced:
Promise.all,AbortController।
এই pattern গুলো — api instance + useAxios hook + service layer + Context/Reducer — যেকোনো production React বা Next.js project-এ সরাসরি ব্যবহার করা যাবে। শুধু endpoint আর field name নিজের backend অনুযায়ী বদলে নিন।
🚀 React Bangla কমিউনিটি
যদি আপনি React, Next.js, Redux, TypeScript এর মতো মর্ডান ফ্রন্টএন্ড টেকনোলজিতে আগ্রহী হন, তাহলে আমাদের Discord সার্ভার এ জয়েন করুন।
🎉 এখনই জয়েন করুন