Search code examples
javascriptreactjsreact-reduxredux-thunk

redux thunk doesnt delete items from the API


My app is a simple bookstore, I am using this API https://www.notion.so/Bookstore-API-51ea269061f849118c65c0a53e88a739 I am able to get the books and post books whitout problems, but when I delete the books it works in the UI but when I refresh the page the books just come back,so is not deleting the books from the API.

this is my reducer

const bookReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD:
      return [
        ...state, action.book,
      ];
    case REMOVE: 
    return state.filter((book) => book.item_id !== action.book.item_id);
    case FETCH_BOOKS: {
      const bookList = [];
      Object.entries(action.books).forEach(([key, value]) => bookList.push({
        item_id: key,
        title: value[0].title,
        author: value[0].author,
        category: value[0].category,
      }));
      return bookList;
    }
    default:
      return state;
  }
};

and this is my actions for deleting the books : first my remove action

export const removeBook = (book) => ({
  type: REMOVE,
  book,
});

and then my action using thunks

export const deleteBooks = createAsyncThunk(REMOVE, async (bookId, thunkAPI) => {
  const response = await fetch(`${BOOK_URL}/${bookId}`,  {
   method: 'DELETE',
    body: JSON.stringify({
      item_id: bookId
    }),
    headers: {
      'Content-Type': 'application/json',
    },
  });
  await thunkAPI.dispatch(removeBook(bookId));
  return response.data;
});

now here is the function where I call the deleteBooks

function Book({title, author, item_id, category}) {
  const dispatch = useDispatch();
  const deleteHandler = (id) =>{
    console.log(id);
    dispatch(deleteBooks(id))
  }
  return (
    <>
      <span>
        {title}
        </span>
        <br/>
        <span>
        {author}
      </span>
      <br/>
      <span>
        {category}
      </span>
      <br/>
      <button
        type="button"
        onClick={() => deleteHandler({item_id})}
      >
        Remove
      </button>
    </>
  );
}

Solution

  • Issue

    From what I can read of the code it appears at some point when initiating the process to remove a book you drop the correct id somewhere along the way.

    1. User clicks "Remove" button and deleteHandler is called and { item_id } value is passed.
    2. deleteHander names argument id
    3. deleteHandler dispatches the deleteBooks action with { item_id } as argument, now named bookId
    function Book({ title, author, item_id, category }) {
      const dispatch = useDispatch();
    
      const deleteHandler = (id) => { // (2) id == { item_id }
        console.log(id);
        dispatch(deleteBooks(id)); // (3)
      }
    
      return (
        <>
          <span>
            {title}
            </span>
            <br/>
            <span>
            {author}
          </span>
          <br/>
          <span>
            {category}
          </span>
          <br/>
          <button
            type="button"
            onClick={() => deleteHandler({ item_id })} // (1)
          >
            Remove
          </button>
        </>
      );
    }
    
    1. deleteBooks consumes bookId and passes it directly in the DELETE requests URL path, and in the request body

    It is at this point I'd expect the DELETE request to fail in the backend since the URL is likely incorrect and the body is incorrect. fetch only throws an error on network errors or cancelled requests though, so I am sure the code just continues along error-free. I'm fairly certain response.data is simply undefined.

    1. deleteBooks dispatches removeBook and passes bookId, still { item_id }, as a payload
    export const deleteBooks = createAsyncThunk(
      REMOVE,
      async (bookId, thunkAPI) => {
        const response = await fetch(`${BOOK_URL}/${bookId}`,  { // (4)
          method: 'DELETE',
          body: JSON.stringify({
            item_id: bookId // (4) { item_id: { item_id } }
          }),
          headers: {
            'Content-Type': 'application/json',
          },
        });
        await thunkAPI.dispatch(removeBook(bookId)); // (5)
        return response.data;
      }
    );
    
    1. removeBook takes { item_id } as a payload value
    export const removeBook = (book) => ({ // (6) { item_id }
      type: REMOVE,
      book, // (6) { item_id } -> action.book.item_id
    });
    
    1. bookReducer reducer function handles REMOVE action and correctly accesses into the action payload to get the nested item_id property and filters the book state array.
    const bookReducer = (state = initialState, action) => {
      switch (action.type) {
        ...
    
        case REMOVE: 
          return state.filter((book) => book.item_id !== action.book.item_id); // (7)
    
        ...
      }
    };
    

    Solution

    Payload/argument naming issues aside, you can likely fix the issue in the deleteBooks action by correctly accessing into the passed "book" object to get the nested item_id property.

    Example:

    export const deleteBooks = createAsyncThunk(
      REMOVE,
      async (book, thunkAPI) => {
        const { item_id } = book // <-- destructure item_id from book object
        const response = await fetch(`${BOOK_URL}/${item_id}`,  { // <-- pass in URL path
         method: 'DELETE',
          body: JSON.stringify({ item_id }), // <-- pass in body
          headers: {
            'Content-Type': 'application/json',
          },
        });
        await thunkAPI.dispatch(removeBook(book)); // <-- pass book object
        return response.data;
      }
    );
    

    Suggestions

    Use consistent naming throughout code

    I suggest editing all these functions/actions/reducers/etc to consistently name the referenced objects throughout the code. It's much easier to keep and maintain the mental mapping of the data flowing through/across the app.

    Since the Book component starts with just a book id, i.e. the item_id prop, then passing a "book id" value around makes a bit more sense.

    function Book({ title, author, item_id, category }) {
      const dispatch = useDispatch();
    
      const deleteHandler = (id) => {
        console.log(id);
        dispatch(deleteBooks(id));
      }
    
      return (
        <>
          <span>
            {title}
            </span>
            <br/>
            <span>
            {author}
          </span>
          <br/>
          <span>
            {category}
          </span>
          <br/>
          <button
            type="button"
            onClick={() => deleteHandler(item_id)} // <-- pass book id
          >
            Remove
          </button>
        </>
      );
    }
    
    export const deleteBooks = createAsyncThunk(
      REMOVE,
      async (bookId, thunkAPI) => {
        const response = await fetch(`${BOOK_URL}/${bookId}`,  {
          method: 'DELETE',
            body: JSON.stringify({ item_id: bookId }),
          headers: {
            'Content-Type': 'application/json',
          },
        });
        await thunkAPI.dispatch(removeBook(bookId));
        return response.data;
      }
    );
    
    export const removeBook = (bookId) => ({
      type: REMOVE,
      bookId,
    });
    
    const bookReducer = (state = initialState, action) => {
      switch (action.type) {
        ...
    
        case REMOVE: 
          return state.filter(
            (book) => book.item_id !== action.bookId // <-- access book id
          );
    
        ...
      }
    };
    

    Check for successful response when removing book

    The the fetch response ok* property to ensure the DELETE request was actually successful.

    Example:

    export const deleteBooks = createAsyncThunk(
      REMOVE,
      async (bookId, thunkAPI) => {
        const response = await fetch(`${BOOK_URL}/${bookId}`,  {
          method: 'DELETE',
            body: JSON.stringify({ item_id: bookId }),
          headers: {
            'Content-Type': 'application/json',
          },
        });
        
        if (response.ok) {
          thunkAPI.dispatch(removeBook(bookId));
        }
        return response.data;
      }
    );
    

    *Note: response.ok is pretty standard but consult the API documentation if success/failure is expressed differently.