Skip to content

[Feature] CRUD API Wrapper Implementation #603

@eslachance

Description

@eslachance

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions