Help with Expense Tracker (step 3 and 7)

Hi, I’m trying to complete the Expense Tracker project, but I ran into a problem. Could someone help me why my editBudget case reducer doesn’t update the state with the new amount from action.payload , and the addTransaction / deleteTransaction case reducer don’t work either.

const budgetsSlice = createSlice({
  name: 'budgets',
  initialState: initialState,
  reducers: {
    editBudget: (state, action) => {
      const index = state.budgets.findIndex((budgetObject) => budgetObject.category === action.payload.category);
      state.budgets[index] = payload;
      return state;
    }
  }
});
const transactionsSlice = createSlice({
  name: 'transactions',
  initialState: initialState,
  reducers: {
    addTransaction: (state, action) => {
      return state.transactions[action.payload.category].push(action.payload);
    },

    deleteTransaction: (state, action) => {
      return state.transactions[action.payload.category].filter(transactionCategory => transactionCategory !== action.payload.category);
    }
  }
});

https://www.codecademy.com/paths/full-stack-engineer-career-path/tracks/fscp-redux/modules/refactoring-with-redux-toolkit/projects/redux-expense-tracker

2 Likes

okay so I figured out where my logic went wrong. I mistakenly though that the state object includes everything, like budgets and transactions properties. I then realized because of the selector function defined at the end of the file, the state keyword only provide access to the budget property. In other words, state gives me an array like this:

[ 
    { category: 'housing', amount: 400 },
    { category: 'food', amount: 100 },
    ...
  ]

so my editBudget case reducer can be written out like so:

editBudget: (state, action) => {
      const index = state.findIndex(obj => obj.category === action.payload.category);
      return state[index] = action.payload;
      }

the transactionsSlice

const transactionsSlice = createSlice({
  name: 'transactions',
  initialState: initialState,
  reducers: {
    addTransaction: (state, action) => {
      state[action.payload.category].push(action.payload);
      return state;
    },

    deleteTransaction: (state, action) => {
      state[action.payload.category].filter(transaction => transaction.id !== action.payload.id);
      return state;
    }
  }
});

of course it can be refactor even more if you’re so inclined :slight_smile:

8 Likes

Hey edpho,

So I was stuck on the same issues but after some tweaking, I realized my issue was that I was “returning” something from these reducers. I tried your solutions and the numbers wouldn’t update. however, when I remove the “return state;” from the reducers it worked. I’m still trying to wrap my head around why a return of some sort isn’t necessary (im assuming it has to do with Immer altering state for us) but just wanted to point it out for anyone else trying this out.

my solution for step 3 looks like this:

editBudget: (state, action) => {
      state.map(budget => {
        if (budget.category === action.payload.category) {
          budget.amount = action.payload.amount
        }
        return budget;
      });
    }

and my solution for step 7 looks like this:

const transactionsSlice = createSlice({
  name: 'transactions',
  initialState: initialState,
  reducers: {
    addTransaction: (state, action) => {
      state[action.payload.category].push(action.payload)
    },
    deleteTransaction: (state, action) => {
      const index = state[action.payload.category].findIndex(tx => tx.id === action.payload.id);
      state[action.payload.category].splice(index, 1);
    }
  }
})
6 Likes

My GIthub link for the Expense Tracker project

Expense Tracker

Before we get started, let’s spend some time using the app in its current implementation to ensure we understand how it’s supposed to work.
Note: I suggest to rebuild our App’s file structure for a more comprehensive understanding (as shown in the first commit).

Create a Budgets Slice

At the top of budgetsSlice.js:

1a. Import createSlice from @reduxjs/toolkit.
Redux Toolkit Documentation

import createSlice from '@reduxjs/toolkit';

Define a slice by calling createSlice() with a configuration object containing the required name, initialState, and reducers properties. Redux Toolkit Documentation

2a. Define a variable, budgetsSlice, and initialize it with a call to createSlice(), passing in an empty configuration object. Do this right after the line defining initialState.

const budgetsSlice = createSlice({

});

2b. Slices are conventionally named for the resource whose state they manage. This slice manages budgets and should be named accordingly. To give the slice a name, add a name property to the configuration object and set it equal to ‘budgets’.

const budgetsSlice = createSlice({
  name: 'budgets',
});

2c. Add an initialState property to the configuration object, and set it equal to the variable initialState that we’ve defined for you.

const budgetsSlice = createSlice({
  name: 'budgets',
  initialState: initialState,
});

2d. Lastly, you’ll need to include a reducers property in the configurations object. For now, set it equal to an empty object.

const budgetsSlice = createSlice({
  name: 'budgets',
  initialState: initialState,
  reducers: {

  },
});

3a. Add an editBudget property to the reducers object passed to createSlice().

const budgetsSlice = createSlice({
  name: 'budgets',
  initialState: initialState,
  reducers: {

  },
});

3b. Set editBudget equal to a case reducer that receives two arguments—state and action . action.payload will have a category and amount property.

const budgetsSlice = createSlice({
  name: 'budgets',
  initialState: initialState,
  reducers: {
    // Set editBudget equal to a case reducer that receives two arguments—state and action
    editBudget: (state, action) => {
      // action.payload will have a category and amount property.
      const {category, amount} = action.payload;
    }
  },
});

3c. editBudget should update the state by finding the budget object whose .category value matches action.payload.category and changing with the .amount value to action.payload.amount.

const budgetsSlice = createSlice({
  name: 'budgets',
  initialState: initialState,
  reducers: {
    // Set editBudget equal to a case reducer that receives two arguments—state and action
    editBudget: (state, action) => {
      // action.payload will have a category and amount property.
      // const {category, amount} = action.payload;
      const category = action.payload.category;
      const amount = action.payload.amount;
      // Update the state by finding the budget object
      // Note: the variables category and action, implemented below, are each assigned action.payload (referenced in the above const). 
      // Ex. category = action.payload.category ;
      // Ex. amount = action.payload.category;
      // The budget object whose .category value matches action.payload.category and changing with the .amount value to action.payload.amount.
      state.find(budget => budget.category === category).amount = amount 
    }
  },
});

Delete your old code and clean up your exports.

4a. Delete the stand-alone editBudget. At the bottom of the file budgetsSlice.js, export the editBudget action creator generated by createSlice() and stored in budgetsSlice.

// export const { myActionCreator } = mySlice.actions;
export const { editBudget } = budgetsSlice.actions;

4b. Delete the stand-alone budgetsReducer, and update the export default statement to export the reducer generated by createSlice() and stored in budgetsSlice.

// export default mySlice.reducer;
export default budgetsSlice.reducer;
Checkpoint 1: We are now able to edit budgets and see out changes reflected in the app.

In transactionsSlice.js:

5a. Import createSlice from @reduxjs/toolkit.

import createSlice from '@reduxjs/toolkit';

Define a slice by calling createSlice() with a configuration object containing the required name, initialState, and reducers properties.

6a. Define a variable, transactionsSlice, and initialize it with a call to createSlice(), passing in an empty configuration object.

const transactionsSlice = createSlice({});

6b. Add a name property to the configuration object and set it equal to ‘transactions’.

const transactionsSlice = createSlice({
  name: 'transactions',
});

6c. Add an initialState property to the configuration object, and set it equal to the variable initialState that we’ve defined for you.

const transactionsSlice = createSlice({
  name: 'transactions',
  initialState: initialState,
});

6d. Lastly, you’ll need to include a reducers property in the configurations object. For now, set it equal to an empty object.

const transactionsSlice = createSlice({
  name: 'transactions',
  initialState: initialState,
  reducers: {},
});

Replace these stand-alone action creators and the reducer with case reducers defined in the object passed to createSlice().

7a. Add an addTransaction property to the reducers object passed to createSlice().

const transactionsSlice = createSlice({
  name: 'transactions',
  initialState: initialState,
  reducers: {
    addTransaction: () => {},

  },
});

7b. Set addTransaction equal to a case reducer that receives two arguments—state and action. It should add the new transaction object (action.payload) to the correct category’s transaction list in the transactions state object.

const transactionsSlice = createSlice({
  name: 'transactions',
  initialState: initialState,
  reducers: {
    addTransaction: (state, action) => {
      // add the new transaction object (action.payload) to the correct category’s transaction list in the transactions state object.
      const category = action.payload.category;
      state[category].push(action.payload);
    },

  },
});

7c. Add a deleteTransaction property to the reducers object passed to createSlice().

const transactionsSlice = createSlice({
  name: 'transactions',
  initialState: initialState,
  reducers: {
    addTransaction: (state, action) => {
      // add the new transaction object (action.payload) to the correct category’s transaction list in the transactions state object.
      const category = action.payload.category;
      state[category].push(action.payload);
    },
    // Add a deleteTransaction property 
    deleteTransaction: () => {
      
    }
  },
});

7d. Set deleteTransaction equal to a case reducer that receives two arguments—state and action. It should delete the old transaction (action.payload) from the correct category’s transaction list in the transactions state object.

const transactionsSlice = createSlice({
  name: 'transactions',
  initialState: initialState,
  reducers: {
    addTransaction: (state, action) => {
      // add the new transaction object (action.payload) to the correct category’s transaction list in the transactions state object.
      const category = action.payload.category;
      state[category].push(action.payload);
    },
    // Add a deleteTransaction property 
    deleteTransaction: (state, action) => {
      // In the deletedIndex in transactionsReducer, action.payload.category and action.payload.id are both used. 
      const id = action.payload.id;
      const category = action.payload.category;
      // It should delete the old transaction (action.payload) from the correct category’s transaction list in the transactions state object.
      // 1. Find the category in `state` that matches the `category` property on `action.payload`
      // 2.  Filter out the old transaction (the transaction with an `id` matching the `id` property on `action.payload`) from that category's transaction array.
      state[category] = state[category].filter(transaction => transaction.id !== id)

    }
  },
});

Delete your old code and clean up your exports.

8a. Delete the stand-alone addTransaction and deleteTransaction,

8b. Export the addTransaction and deleteTransaction action creators generated by createSlice()and stored in transactionsSlice.

export { addTransaction, deleteTransaction } from transactionsSlice.action;

8c. Delete the stand-alone transactionsReducer,

8d. Update the export default statement to export the reducer generated by createSlice() and stored in transactionsSlice.

export default transactionsSlice.reducer;
34 Likes

In the final step, where you delete the transaction you clicked, I think you shouldn’t use filter method, because using this method will not alter the original array.

state[category] = state[category].filter(transaction => transaction.id !== id)

// using splice method to alter the original array
const deletedIndex = state[action.payload.category].findIndex(transaction => transaction.id === action.payload.id);
state[action.payload.category].splice(deletedIndex,1)

Your code is too confusing.
You’re declaring variables like category and amount while these are defined in the state objects as well.
It still works but it’s not the appropriate way.

I suggest that people looking for solutions, to find a better understandable one.

1 Like

you make a really good point weeshin.

Actually I checked on this. The official redux document says the following: Immer expects that you will either mutate the existing state, or construct a new state value yourself and return it, but not both in the same function! However, it is possible to use immutable updates to do part of the work and then save the results via a “mutation”. An example of this might be filtering a nested array:

Looking at kenneth’s code, amazing job kenneth! by the way, you helped me alot with your code in this project.
Notice how he writes:

state[category] = state[category].filter...

That is technically allowed. He could have also written:

return state[category].filter(transaction => ...
2 Likes

Thought I would add my code as well. First-timer here.

import {createSlice} from '@reduxjs/toolkit';

export const CATEGORIES = ['housing', 'food', 'transportation', 'utilities', 'clothing', 'healthcare', 'personal', 'education', 'entertainment'];
const initialState = CATEGORIES.map(category => ({ category: category, amount: 0 }))


const budgetsSlice = createSlice({
  name: 'budgets',
  initialState: initialState,
  reducers: {
    editBudget: (state, action) {
      return state.map((budgetItem) => {
         if (budgetItem.category === action.payload.category) {
           budgetItem.amount = action.payload.amount;
           };
         return budgetItem;
         });
    }
  }
});

export const {editBudget} = budgetsSlice.actions;
export default budgetsSlice.reducer;

export const selectBudgets = (state) => state.budgets;
export {budgetsSlice.reducer as budgetsReducer};

And here is for the transactionsSlice…

import {createSlice} from '@reduxjs/toolkit';

export const CATEGORIES = ['housing', 'food', 'transportation', 'utilities', 'clothing', 'healthcare', 'personal', 'education', 'entertainment'];
const initialState = Object.fromEntries(CATEGORIES.map(category => [category, []]));

const transactionsSlice = createSlice({
  name: 'transactions',
  initialState: initialState,
  reducers: {
    addTransaction: (state, action) => {
      let {category} = action.payload;
      state[category].push(action.payload);
      return state;
    },


  deleteTransaction: (state, action) => {
    let {category, id} = action.payload;
    let newArr = state[category].filter(trans => trans.id !== id);
    state[category] = newArr;
    return state;
  }

  }
});

export const {addTransaction, deleteTransaction} = transactionsSlice.actions;

export default transactionsSlice.reducer;

export const selectTransactions = (state) => state.transactions;
export const selectFlattenedTransactions = (state) => Object.values(state.transactions).reduce((a,b) => [...a, ...b], []);
2 Likes

Thank you so much for breaking down each step in this project! It was super helpful!

1 Like

I really am not the best at logic, so thank you so much for this step by step help!!!

1 Like

Thank you - very helpful.

thank you so much for showing your code with all comments and explanations

1 Like

Thanks for the discussion! I found it very helpful while working on this project.

1 Like
Hello World

Here is my source code for the project.

1 Like

No need to complicate it with methods and/or loops. This is what I did:

const budgetSlice = createSlice({
  name: 'budgets',
  initialState: initialState,
  reducers: {
    editBudget: (state, action) => {
      //might not work but worth trying
      return state[action.payload.category].amount = payload.amount, 
    },
  }
});

figured it was too simple and wouldn’t work but it does

Or maybe it doesn’t. I have solved it before but reset the task to do it again. Problem is it seems to keep working no matter what I do to the code. I discovered a spelling mistake in one of the exports so I trie removing all exports in the files we work in. It still works.