Skip to content

Instantly share code, notes, and snippets.

@ARAldhafeeri
Created December 4, 2024 19:18
Show Gist options
  • Save ARAldhafeeri/1ad10710bee110b9a88013984272fbbd to your computer and use it in GitHub Desktop.
Save ARAldhafeeri/1ad10710bee110b9a88013984272fbbd to your computer and use it in GitHub Desktop.
higher order redux toolkit function that generate redundant logic when the APIs are standardized.
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import instance from "../instance";
import { message } from "antd";
import { createListenerMiddleware, isAnyOf } from "@reduxjs/toolkit";
const createEntitySlice = ({
entityName,
endpoints,
extraReducers = () => {},
extraThunks ,
extraInitialState,
extraActions,
middlewareEnhancements = (middleware, thunks) => middleware,
}) => {
const initialState = {
data: [],
loading: true,
error: null,
limit: 10,
page: 1,
total: 1,
pageSize: 5,
search: "",
filter: "ALL",
stats: [],
};
// conditional extra state
Object.assign(initialState, extraInitialState);
// Base Thunks
const fetchEntity = createAsyncThunk(`${entityName}/fetch`, async (params, thunkAPI) => {
try {
const res = await instance.get(endpoints.FETCH(params.limit, params.page));
return res.data;
} catch (error) {
console.error(`Error creating ${entityName}:`, error); // Log the error
return thunkAPI.rejectWithValue(error?.response?.data?.message);
}
});
const createEntity = createAsyncThunk(`${entityName}/create`, async (data, thunkAPI) => {
try {
const res = await instance.post(endpoints.BASE, data);
return res.data;
} catch (error) {
console.error(`Error creating ${entityName}:`, error); // Log the error
return thunkAPI.rejectWithValue(error?.response?.data?.message);
}
});
const updateEntity = createAsyncThunk(`${entityName}/update`, async (data, thunkAPI) => {
try {
const res = await instance.put(endpoints.WITH_ID(data.id), data);
return res.data;
} catch (error) {
console.error(`Error creating ${entityName}:`, error); // Log the error
return thunkAPI.rejectWithValue(error?.response?.data?.message);
}
});
const deleteEntity = createAsyncThunk(`${entityName}/delete`, async (data, thunkAPI) => {
try {
const res = await instance.delete(endpoints.WITH_ID(data.id));
return res.data;
} catch (error) {
console.error(`Error creating ${entityName}:`, error); // Log the error
return thunkAPI.rejectWithValue(error?.response?.data?.message);
}
});
const searchEntity = createAsyncThunk(`${entityName}/search`, async (params, thunkAPI) => {
try {
const res = await instance.post(endpoints.SEARCH(params.limit, params.page), { payload: params.payload });
return res.data;
} catch (error) {
console.error(`Error creating ${entityName}:`, error); // Log the error
return thunkAPI.rejectWithValue(error?.response?.data?.message);
}
});
// Combine Base Thunks with Additional Thunks
const thunks = {
fetchEntity,
createEntity,
updateEntity,
deleteEntity,
searchEntity,
};
// conditional extra thunks
Object.assign(thunks, extraThunks);
// Slice
const entitySlice = createSlice({
name: entityName,
initialState,
reducers: Object.assign({
searchUpdate: (state, action) => {
state.search = action.payload;
},
searchReset: (state) => {
state.search = "";
state.limit = 5;
state.page = 1;
}
}, extraActions),
extraReducers: (builder) => {
builder
.addCase(fetchEntity.pending, (state) => {
state.loading = true;
})
.addCase(fetchEntity.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload.data.data;
state.total = action.payload.data.total;
state.page = action.payload.data.page;
state.pageSize = action.payload.data.pageSize;
})
.addCase(fetchEntity.rejected, (state) => {
state.loading = false;
state.error = true;
})
.addCase(createEntity.fulfilled, (state, action) => {
state.data.push(action.payload.data);
})
.addCase(updateEntity.fulfilled, (state, action) => {
const index = state.data.findIndex((item) => item._id === action.payload.data._id);
if (index !== -1) state.data[index] = { ...action.payload.data };
})
.addCase(deleteEntity.fulfilled, (state, action) => {
state.data = state.data.filter((item) => item._id !== action.payload.data._id);
})
.addCase(searchEntity.fulfilled, (state, action) => {
state.data = action.payload.data.data;
state.total = action.payload.data.total;
state.page = action.payload.data.page;
state.pageSize = action.payload.data.pageSize;
});
// Extend Reducers
extraReducers(builder, thunks);
}
});
// Middleware
const listenerMiddleware = createListenerMiddleware();
listenerMiddleware.startListening({
matcher: isAnyOf(thunks.createEntity.rejected, thunks.updateEntity.rejected, thunks.deleteEntity.rejected, thunks.searchEntity.rejected),
effect: async (action) => {
message.error(action.payload?.message);
}
});
listenerMiddleware.startListening({
matcher: isAnyOf(thunks.createEntity.fulfilled, thunks.updateEntity.fulfilled, thunks.deleteEntity.fulfilled),
effect: async (action) => {
message.success(action.payload?.message);
}
});
listenerMiddleware.startListening({
predicate: (_action, currentState, previousState) => {
const entityState = currentState[entityName];
return entityState.search !== previousState[entityName].search && entityState.search !== "";
},
effect: async (_action, listenerApi) => {
listenerApi.cancelActiveListeners();
await listenerApi.delay(500);
const state = listenerApi.getState()[entityName];
listenerApi.dispatch(thunks.searchEntity({ limit: state.limit, page: state.page, payload: state.search }));
}
});
listenerMiddleware.startListening({
actionCreator: entitySlice.actions.searchReset,
effect: async (_action, listenerApi) => {
listenerApi.cancelActiveListeners();
await listenerApi.delay(200);
listenerApi.dispatch(thunks.fetchEntity({ limit: 10, page: 1 }));
}
});
// Apply Middleware Enhancements
const enhancedMiddleware = middlewareEnhancements ? middlewareEnhancements(listenerMiddleware, thunks) : listenerMiddleware;
return {
reducer: entitySlice.reducer,
actions: entitySlice.actions,
thunks,
middleware: enhancedMiddleware
};
};
export default createEntitySlice;
// example of using extra thunks, initstate
/**
* const customStatsThunk = createAsyncThunk("reservation/stats", async (_, thunkAPI) => {
* try {
* const res = await instance.get(ENDPOINTS.SUBSCRIBED.RESERVATION.STATS);
* return res.data;
* } catch (error) {
* return thunkAPI.rejectWithValue(error?.response?.data?.message);
* }
* });
*
* // Reservation Slice
* const reservationSlice = createEntitySlice({
* entityName: "reservation",
* endpoints: ENDPOINTS.SUBSCRIBED.RESERVATION,
* extraInitialState: {
* stats: {}
* },
* extraThunks: {
* fetchStats: customStatsThunk
* },
* extraReducers: (builder, thunks) => {
* builder.addCase(thunks.fetchStats.fulfilled, (state, action) => {
* state.stats = action.payload.data;
* });
* },
* middlewareEnhancements: (middleware, thunks) => {
* middleware.startListening({
* actionCreator: thunks.fetchStats.fulfilled,
* effect: async (action) => {
* message.success("Stats loaded successfully!");
* }
* });
* return middleware;
* }
* });
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment