Skip to content

Commit 14194ff

Browse files
committed
feat: Add Rtk Axios plugin in NextJs
1 parent 1d0a9ee commit 14194ff

File tree

4 files changed

+371
-17
lines changed

4 files changed

+371
-17
lines changed

src/operation/stateManagementCachingSol/reduxThunkAxios/reduxThunk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { writeFileFromConfig } from "@/utils/file";
22
import getCurrentProject from "@/operation/getProjectType";
33
import { SupportedProjectType } from "@/types";
44
import ReduxAxiosReactPlugin from "@/plugins/react/reduxThunkAxios/config";
5+
import ReduxThunkNextPlugin from "@/plugins/nextjs/reduxThunkAxios/config";
56

67
export default async function addReduxThunkWithAxios() {
78
const projectType = getCurrentProject();
@@ -23,4 +24,5 @@ async function addReduxInReact() {
2324
}
2425

2526
async function addReduxInNext() {
27+
await writeFileFromConfig(ReduxThunkNextPlugin)
2628
}
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
import GlobalStateUtility from "@/global";
2+
import { PluginConfigType, FileType } from "@/types";
3+
import { getRegexForRootComponent } from "@/utils/fileManipulation";
4+
5+
const envExFileContent = "NEXT_PUBLIC_BASE_URL";
6+
7+
const envFileContent = `NEXT_PUBLIC_BASE_URL=https://jsonplaceholder.typicode.com`;
8+
9+
function getStoreConfig(isTsProject: boolean) {
10+
return `
11+
import { configureStore } from "@reduxjs/toolkit";
12+
import counterSlice from "./features/counterSlice";
13+
import postSlice from "./features/postSlice";
14+
${isTsProject? `import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";`: ""}
15+
16+
export const store = configureStore({
17+
reducer: {
18+
counter: counterSlice,
19+
post:postSlice
20+
},
21+
});
22+
${isTsProject? `export type RootState = ReturnType<typeof store.getState>;
23+
export type AppDispatch = typeof store.dispatch;
24+
25+
// Use throughout your app instead of plain "useDispatch" and "useSelector"
26+
export const useAppDispatch = () => useDispatch<AppDispatch>();
27+
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;`
28+
: ""
29+
}
30+
`;
31+
}
32+
33+
function getCounterSliceConfig(isTsProject: boolean) {
34+
return `import { createSlice${
35+
isTsProject ? ", PayloadAction " : ""
36+
}} from "@reduxjs/toolkit";
37+
${isTsProject? `export interface CounterState {
38+
value: number;
39+
incrementAmount: number;
40+
}`: ""
41+
}
42+
43+
const initialState${isTsProject ? ": CounterState " : ""}= {
44+
value: 0,
45+
incrementAmount: 1,
46+
};
47+
48+
export const counterSlice = createSlice({
49+
name: "counter",
50+
initialState,
51+
reducers: {
52+
increment: state => {
53+
state.value += state.incrementAmount;
54+
},
55+
decrement: state => {
56+
state.value -= state.incrementAmount;
57+
},
58+
changeIncrementAmount: (state, action${
59+
isTsProject ? ": PayloadAction<number>" : ""
60+
}) => {
61+
state.incrementAmount = action.payload;
62+
},
63+
},
64+
});
65+
66+
export const { increment, decrement, changeIncrementAmount } =
67+
counterSlice.actions;
68+
69+
export default counterSlice.reducer;
70+
`;
71+
}
72+
73+
function getPostSlice(isTsProject: boolean) {
74+
const projectType =
75+
GlobalStateUtility.getInstance().getCurrentProjectGenType();
76+
return `import { createAsyncThunk, createSlice ${
77+
isTsProject ? ", PayloadAction " : ""
78+
} } from "@reduxjs/toolkit";
79+
import { getPosts ${isTsProject?",PostType":""} } from "@/store/api/api";
80+
export const getPostApi = createAsyncThunk("posts", async () => {
81+
try {
82+
const response = await getPosts();
83+
return response;
84+
} catch (error) {
85+
// you can create util functon to handle errors.
86+
return Promise.reject(error);
87+
}
88+
});
89+
90+
${isTsProject? `type PostState = {
91+
loading: boolean;
92+
error: string | undefined;
93+
posts: PostType[];
94+
};`: ""
95+
}
96+
const initialState${isTsProject?`: PostState`:""} = { posts: [], loading: false, error: "" };
97+
export const postSlice = createSlice({
98+
name: "posts",
99+
initialState,
100+
reducers: {},
101+
extraReducers(builder) {
102+
builder.addCase(getPostApi.pending, (state) => {
103+
state.loading = true;
104+
});
105+
builder.addCase(getPostApi.fulfilled, (state, action${isTsProject?`:PayloadAction<PostType[]>`:""}) => {
106+
state.loading = false;
107+
state.posts = action.payload;
108+
state.error = "";
109+
});
110+
builder.addCase(getPostApi.rejected, (state, action) => {
111+
state.loading = false;
112+
state.posts = [];
113+
state.error = action.error.message;
114+
});
115+
},
116+
});
117+
export default postSlice.reducer;
118+
`
119+
.replaceAll(/\\/g, "")
120+
.replaceAll("+=+", "`");
121+
}
122+
123+
function getApi(isTsProject: boolean) {
124+
return `import axios from "axios";
125+
import Cookies from "js-cookie";
126+
127+
/*
128+
Best practice to store access-token and refresh-token is
129+
cookie not a local storage.
130+
131+
Here I've created request interceptors to intercept
132+
request and add token into request header from cookie.
133+
You can update this logic as well create response interceptors based on project requirements.
134+
135+
I've added custom config called withoutAuth in axios instance
136+
withoutAuth config value will decide whether send a token in request or not.
137+
if API need token in header in that case yu don't have to pass withoutAuth config
138+
and it will work as expected.
139+
EX:
140+
API.get('/users')
141+
API.post('/posts', { title: 'foo', body: 'bar', userId: 1 })
142+
143+
When you don't need token in header at that time you've to pass withoutAuth true.
144+
EX:
145+
API.get('/users', { withoutAuth: true})
146+
API.post('/posts', { title: 'foo', body: 'bar', userId: 1 }, { withoutAuth: true })
147+
*/
148+
${isTsProject?`declare module "axios" {
149+
export interface AxiosRequestConfig {
150+
withoutAuth?: boolean;
151+
}
152+
}`:``
153+
}
154+
export const API = axios.create({
155+
baseURL: process.env.NEXT_PUBLIC_BASE_URL,
156+
withoutAuth: false,
157+
headers: {
158+
"Content-Type": "application/json",
159+
},
160+
/*
161+
you can pass common config here.
162+
*/
163+
});
164+
165+
/**
166+
* If your backend has refresh and access token then to refresh the access token you can add logic to the below function
167+
* can save access token in memory and refresh token in cookie
168+
*/
169+
// Define a function to refresh the token
170+
const refreshToken = async () => {
171+
// try {
172+
// const response = await axios.post("YOUR_REFRESH_TOKEN_ENDPOINT", {
173+
// refreshToken: Cookies.get("refreshToken"), // Load the refreshToken from cookies or if https cookie then just make get request to your endpoint
174+
// });
175+
// const newAccessToken = response.data.accessToken;
176+
// Cookies.set("accessToken", newAccessToken, { path: "/" });
177+
// return newAccessToken;
178+
// } catch (error) {
179+
// throw error;
180+
// }
181+
};
182+
183+
// A request interceptor to inject the access token into requests
184+
API.interceptors.request.use(
185+
(config) => {
186+
const accessToken = Cookies.get("accessToken"); // Load the access token from cookies or local storage
187+
188+
if (!config.withoutAuth && accessToken) {
189+
config.headers["Authorization"] = +=+Bearer \${accessToken}+=+;
190+
}
191+
192+
return config;
193+
},
194+
(error) => {
195+
// Handle request errors here
196+
197+
return Promise.reject(error);
198+
},
199+
);
200+
201+
// A response interceptor to handle token expiration and auto-refresh
202+
API.interceptors.response.use(
203+
(response) => {
204+
// Modify the response data here
205+
return response;
206+
},
207+
async (error) => {
208+
if (error.response && error.response.status === 401) {
209+
// Token expired, try to refresh the token
210+
const newAccessToken = await refreshToken();
211+
212+
// Update the original request with the new access token
213+
const originalRequest = error.config;
214+
originalRequest.headers["Authorization"] =+=+Bearer \${newAccessToken}+=+;
215+
216+
// Retry the original request
217+
return axios(originalRequest);
218+
}
219+
220+
return Promise.reject(error);
221+
},
222+
);
223+
224+
//post type
225+
${isTsProject?` export type PostType = {
226+
userId: number;
227+
id: number;
228+
title: string;
229+
body: string;
230+
};`:""}
231+
232+
233+
//get posts
234+
export const getPosts = async () => API.get ${ isTsProject? "<PostType[]>":""}("/posts", { withoutAuth: true }).then((res) => res.data);
235+
236+
//create post
237+
export const createPost = (body ${isTsProject?":{ heading: string; content: string }":""}) => API.post("/item/create", body);
238+
239+
`
240+
.replaceAll(/\\/g, "")
241+
.replaceAll("+=+", "`");
242+
}
243+
244+
export const StoreProviderConfig = (isTsProject: boolean) => `"use client";
245+
import React from "react";
246+
import { store } from "./index${""}";
247+
import { Provider } from "react-redux";
248+
249+
export default function StoreProvider({
250+
children,
251+
}${isTsProject? `: {
252+
children: React.ReactNode;
253+
}`
254+
: ""
255+
}) {
256+
return <Provider store={store}>{children}</Provider>;
257+
}
258+
`;
259+
260+
export const rtkQueryExampleNext =(isTsProject:boolean)=> `"use client";
261+
import React, { useEffect } from "react";
262+
${isTsProject?
263+
`import { useAppDispatch, useAppSelector } from "@/store/index";`:`import { useDispatch, useSelector } from "react-redux";`
264+
}
265+
import { getPostApi } from "@/store/features/postSlice";
266+
267+
const QueryExample = () => {
268+
269+
const dispatch = ${isTsProject?"useAppDispatch();":"useDispatch();"}
270+
271+
const { posts, loading } = ${isTsProject?"useAppSelector":"useSelector"}((state) => state.post);
272+
273+
useEffect(() => {
274+
dispatch(getPostApi());
275+
}, []);
276+
277+
return (
278+
<div>
279+
{loading && <div>Loading...</div>}
280+
{posts &&
281+
posts?.map((post) => {
282+
return <div key={post.id}>{post.title}</div>;
283+
})}
284+
</div>
285+
);
286+
};
287+
288+
export default QueryExample;
289+
290+
`;
291+
292+
const ReduxThunkNextPlugin: PluginConfigType = {
293+
initializingMessage: "Adding RTk axios with Redux Store, Please wait !",
294+
files: [
295+
{
296+
content: envFileContent,
297+
fileName: ".env",
298+
fileType: FileType.SIMPLE,
299+
path: [],
300+
},
301+
{
302+
content: envExFileContent,
303+
fileName: ".env.example",
304+
fileType: FileType.SIMPLE,
305+
path: [],
306+
},
307+
{
308+
content: getStoreConfig,
309+
fileName: "index",
310+
fileType: FileType.NATIVE,
311+
path: ["src", "store"],
312+
},
313+
{
314+
content: getCounterSliceConfig,
315+
fileName: "counterSlice",
316+
fileType: FileType.NATIVE,
317+
path: ["src", "store", "features"],
318+
},
319+
{
320+
content: getPostSlice,
321+
fileName: "postSlice",
322+
fileType: FileType.NATIVE,
323+
path: ["src", "store", "features"],
324+
},
325+
{
326+
content: getApi,
327+
fileName: "api",
328+
fileType: FileType.NATIVE,
329+
path: ["src", "store", "api"],
330+
},
331+
{
332+
content: StoreProviderConfig,
333+
fileName: "StoreProvider",
334+
fileType: FileType.COMPONENT,
335+
path: ["src", "store"],
336+
},
337+
{
338+
content: rtkQueryExampleNext,
339+
fileName: "page",
340+
fileType: FileType.COMPONENT,
341+
path: ["src", "app", "users"],
342+
},
343+
],
344+
dependencies: function (isTsProject: boolean) {
345+
return `@reduxjs/toolkit react-redux js-cookie axios ${
346+
isTsProject ? "@types/js-cookie" : ""
347+
}`;
348+
},
349+
fileModification: {
350+
Page: {},
351+
Layout: {
352+
importStatements: `import StoreProvider from "@/store/StoreProvider";`,
353+
addBeforeMatch: "<StoreProvider>",
354+
addAfterMatch: "</StoreProvider>",
355+
regex: getRegexForRootComponent("html"),
356+
examplePath:"/users",
357+
exampleName:"RTK Axios Example"
358+
},
359+
},
360+
successMessage: "Successfully added rtk-query with redux !",
361+
};
362+
export default ReduxThunkNextPlugin;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {default} from "./config"

0 commit comments

Comments
 (0)