@mahdi209/apithunk
v1.0.0
Published
Zero-boilerplate Redux Toolkit API thunks + toast notifications
Maintainers
Readme
rtk-api-kit
مكتبة خفيفة تُغلّف @reduxjs/toolkit + axios لإنشاء API thunks مع Toast notifications بأقل كود ممكن.
التثبيت
npm install rtk-api-kit
# أو
yarn add rtk-api-kitPeer dependencies — يجب أن تكون موجودة في مشروعك:
npm install @reduxjs/toolkit axios react
الإعداد (مرة واحدة)
1 — Configure at app startup
// src/app/apiKit.ts
import { configureApiKit } from "rtk-api-kit";
configureApiKit({
baseURL: import.meta.env.VITE_API_BASE_URL,
withCredentials: true, // إرسال HttpOnly cookies
defaultHeaders: { "Accept-Language": "ar" }, // اختياري
});استدعِ هذا الملف في main.tsx قبل أي شيء آخر:
// src/main.tsx
import "./app/apiKit"; // ← أول سطر
import { store } from "./app/store";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
);2 — أضف toastReducer للـ store
// src/app/store.ts
import { configureStore } from "@reduxjs/toolkit";
import { toastReducer } from "rtk-api-kit";
import booksReducer from "@/features/books/booksSlice";
export const store = configureStore({
reducer: {
toast: toastReducer, // ← المطلوب
books: booksReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;الاستخدام
إنشاء thunks
// src/features/books/booksThunks.ts
import { createApiThunk } from "rtk-api-kit";
import type { Book, BooksQueryParams, PaginatedResponse } from "@/types/book";
/** GET /api/Book — paginated list */
export const fetchBooksThunk = createApiThunk<PaginatedResponse<Book>, BooksQueryParams>(
"books/fetchAll",
"GET",
"/api/Book",
{ showToast: false },
);
/** GET /api/Book/:id */
export const fetchBookByIdThunk = createApiThunk<Book, string>(
"books/fetchById",
"GET",
(id) => `/api/Book/${id}`,
{ showToast: false },
);
/** POST /api/Book — FormData */
export const createBookThunk = createApiThunk<Book, FormData>(
"books/create",
"POST",
"/api/Book",
{
successMessage: "تمت إضافة الكتاب بنجاح",
serverMessageStatuses: [400], // يظهر خطأ الـ server في الـ toast
},
);
/** PUT /api/Book/:id */
export const updateBookThunk = createApiThunk<Book, { id: string; data: FormData }>(
"books/update",
"PUT",
({ id }) => `/api/Book/${id}`,
{
successMessage: "تم تحديث الكتاب بنجاح",
serverMessageStatuses: [400],
getBody: ({ data }) => data,
},
);
/** DELETE /api/Book/:id */
export const deleteBookThunk = createApiThunk<void, string>(
"books/delete",
"DELETE",
(id) => `/api/Book/${id}`,
{ successMessage: "تم حذف الكتاب بنجاح" },
);
/** تنزيل ملف PDF */
export const downloadBookPdfThunk = createApiThunk<void, string>(
"books/downloadPdf",
"GET",
(id) => `/api/Book/${id}/pdf`,
{
fileType: "pdf",
fileName: (id) => `book-${id}`,
},
);Redux slice
// src/features/books/booksSlice.ts
import { createSlice } from "@reduxjs/toolkit";
import type { Book } from "@/types/book";
import {
fetchBooksThunk,
fetchBookByIdThunk,
} from "./booksThunks";
interface BooksState {
items: Book[];
currentPage: number;
pagesCount: number;
totalCount: number;
isLast: boolean;
listLoading: boolean;
selected: Book | null;
detailLoading: boolean;
}
const initialState: BooksState = {
items: [], currentPage: 1, pagesCount: 1, totalCount: 0,
isLast: true, listLoading: false, selected: null, detailLoading: false,
};
const booksSlice = createSlice({
name: "books",
initialState,
reducers: {
clearSelectedBook: (state) => { state.selected = null; },
},
extraReducers: (builder) => {
builder
.addCase(fetchBooksThunk.pending, (state) => { state.listLoading = true; })
.addCase(fetchBooksThunk.fulfilled, (state, { payload }) => {
state.listLoading = false;
if (payload) {
state.items = payload.data;
state.currentPage = payload.currentPage;
state.pagesCount = payload.pagesCount;
state.totalCount = payload.totalCount;
state.isLast = payload.isLast;
}
})
.addCase(fetchBooksThunk.rejected, (state) => { state.listLoading = false; })
.addCase(fetchBookByIdThunk.pending, (state) => { state.detailLoading = true; state.selected = null; })
.addCase(fetchBookByIdThunk.fulfilled, (state, { payload }) => {
state.detailLoading = false;
state.selected = payload ?? null;
})
.addCase(fetchBookByIdThunk.rejected, (state) => { state.detailLoading = false; });
},
});
export const { clearSelectedBook } = booksSlice.actions;
export default booksSlice.reducer;Toast component
// src/features/Toast/ToastContainer.tsx
import { useSelector, useDispatch } from "react-redux";
import { removeToast } from "rtk-api-kit";
import type { RootState } from "@/app/store";
export default function ToastContainer() {
const dispatch = useDispatch();
const toasts = useSelector((s: RootState) => s.toast.toasts);
return (
<div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 flex flex-col gap-2">
{toasts.map((t) => (
<div
key={t.id}
className={`px-4 py-3 rounded shadow text-white text-sm
${t.type === "success" ? "bg-green-600" :
t.type === "error" ? "bg-red-600" :
t.type === "warning" ? "bg-yellow-500" : "bg-blue-600"}`}
onClick={() => dispatch(removeToast(t.id))}
>
{t.message}
</div>
))}
</div>
);
}خيارات createApiThunk
| الخيار | النوع | الافتراضي | الوصف |
|---|---|---|---|
| showToast | boolean | true | إظهار toast عند النجاح والفشل |
| successMessage | string | رسالة افتراضية | رسالة النجاح |
| errorMessage | string | رسالة افتراضية | رسالة الفشل |
| fileType | "pdf" \| "excel" \| "word" | — | تفعيل تنزيل ملف |
| fileName | string \| (arg) => string | "ملف" | اسم الملف المنزّل |
| headers | Record \| (arg) => Record | — | headers إضافية لهذا الطلب فقط |
| statusMessages | { [statusCode]: string \| fn } | — | رسائل مخصصة لكل status code |
| silentStatuses | number[] | [] | status codes لا تُظهر toast |
| serverMessageStatuses | number[] | [] | status codes يُستخدم فيها خطأ الـ server |
| getBody | (arg) => unknown | — | تحويل الـ arg إلى body |
خيارات configureApiKit
| الخيار | النوع | الافتراضي | الوصف |
|---|---|---|---|
| baseURL | string | مطلوب | Base URL لجميع الطلبات |
| withCredentials | boolean | true | إرسال cookies مع الطلبات |
| defaultHeaders | Record<string, string> | — | headers افتراضية لجميع الطلبات |
| requestInterceptor | (config) => config | — | Axios request interceptor مخصص |
| responseErrorInterceptor | (error) => Promise | — | Axios error interceptor (للـ refresh token مثلاً) |
مثال: Refresh Token Interceptor
configureApiKit({
baseURL: import.meta.env.VITE_API_BASE_URL,
responseErrorInterceptor: async (error) => {
if ((error as any)?.response?.status === 401) {
await store.dispatch(refreshTokenThunk());
}
return Promise.reject(error);
},
});البناء المحلي
npm run build # بناء للإنتاج
npm run dev # watch mode أثناء التطويرالنشر على npm
npm login
npm publish --access public