-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
I've been working on a fresh new project and this gave me the benefit of being able to not only use redux-toolkit, but also the freedom to write a "builder" for our multiple CRUD endpoints.
After speaking with @markerikson , it became clear this might be something that's useful to a growing number of people. The basics of this is that the API endpoints we have are all similar in structure, and since CRUD operations usually have the same shape, I quickly ended up with a lot of duplicated code for each endpoint, where the only difference was the endpoint name.
So I made a function that creates all of the thunks using createAsyncThunk
and the customReducer entries to make them work. This is then returned and used in configureStore or in the various components that require the data.
It's a little verbose as one might expect, but it works:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export default ({
baseUrl,
name,
}) => {
const fetchById = createAsyncThunk(
`${name}/fetchByIdStatus`,
id => fetch(`${baseUrl}/${id}`).then(r => r.json()),
);
const fetchAll = createAsyncThunk(
`${name}/fetchAllStatus`,
() => fetch(`${baseUrl}/`).then(r => r.json()),
);
const updateById = createAsyncThunk(
`${name}/updateByIdStatus`,
async ({id, data}) => {
await fetch(`${baseUrl}/${id}`, {
method: "UPDATE",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then(r => r.json());
return data;
},
);
const deleteById = createAsyncThunk(
`${name}/deleteByIdStatus`,
id => fetch(`${baseUrl}/${id}`, {
method: 'DELETE',
}).then(r => r.json()).then(() => id),
);
const createNew = createAsyncThunk(
`${name}/createNewStatus`,
data => fetch(`${baseUrl}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}),
);
const slice = createSlice({
name,
initialState: { entities: {}, loading: 'idle'},
reducers: {},
extraReducers: {
[fetchById.fulfilled]: (state, action) => {
state.entities[action.payload.id] = action.payload;
},
[fetchById.rejected]: (state, action) => {
state.entities[action.payload.id] = action.payload;
},
[updateById.fulfilled]: (state, action) => {
state.entities[action.payload.id] = action.payload;
},
[deleteById.fulfilled]: (state, action) => {
delete state.entities[action.payload.id];
return state;
},
[createNew.fulfilled]: (state, action) => {
state.entities[action.payload.id] = action.payload;
},
[fetchAll.fulfilled]: (state, action) => {
state.entities = {
...state.entities,
...action.payload,
};
},
},
});
return {
reducer: slice.reducer,
fetchById,
fetchAll,
updateById,
deleteById,
createNew,
};
};
This is called simply by providing the baseUrl and the name (in our case the could be different so that's why we had to split those 2 arguments):
export const cascades = builder({
baseUrl: `${baseUrl}/cascade-blocks`,
name: 'cascades',
});
export const groups = builder({
baseUrl: `${baseUrl}/groups`,
name: 'groups',
});
And then I imported those into our configureStore, combining them as a root reducer:
import { cascades, groups } from './slices';
const rootReducer = combineReducers( {
cascades: cascades.reducer,
groups: groups.reducer,
} );
export default store = configureStore({ reducer: rootReducer });
The only thing missing from the above is my next step, which is to provide some selector functions that can getById, getAll, getIDs, and other useful related things.
After adding the selectors I'll consider this to be a fairly self-contained, re-usable module that I'm sure we'll start using internally. Hopefully, it can be of service for Redux-Toolkit also!