Search code examples
javascriptreactjsreduxreact-hooksreact-redux

How is my useDispatch hook set up incorrectly here?


I have just started learning Redux with React so I apologize if it is something as simple as syntax. This project prompts me to use a variety of react-redux functions.

I have my slice below:

/** @type {{balance: number, history: Transaction[]}} */
const initialState = {balance: 0, history: []};

/* TODO
Add two reducers  to the transactions slice: "deposit" and "transfer".
Both reducers update the balance and then record the transaction.

"deposit" should increase the balance by the amount in the payload,
while "transfer" should decrease the balance by the amount in the payload.

Refer to the "withdrawal" reducer, which is already implemented for you.
*/

const transactionsSlice = createSlice({
  name: "transactions",
  initialState,
  reducers: {
    withdrawal: (state, { payload }) => {
      state.balance -= payload.amount;
      state.history.push({
        type: "withdrawal",
        amount: payload.amount,
        balance: state.balance,
      });
    },
    deposit: (state, { payload }) => {
      state.balance += payload.amount;
      state.history.push({
        type: "deposit",
        amount: payload.amount,
        balance: state.balance,
      });
    },
    transfer: (state, { payload }) => {
      state.balance -= payload.amount;
      state.history.push({
        type: "transfer",
        amount: payload.amount,
        balance: state.balance,
      });
    },
  },
});

export const { deposit, withdrawal, transfer } = transactionsSlice.actions;

export const selectBalance = (state) => state.transactions.balance;
export const selectHistory = (state) => state.transactions.history;

export default transactionsSlice.reducer;

My store:

import { configureStore } from "@reduxjs/toolkit";
import transactionsSlice from "../features/transactions/transactionsSlice";

// TODO: Configure the store to use the reducer from the transactions slice.
export const store = configureStore({
    reducer:{
        transactions:transactionsSlice,
    }
});

And a Transactions.jsx where I am using useDispatch:

import { useState } from "react";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";

import "./transactions.scss";

/**
 * Allows users to deposit to, withdraw from, and transfer money from their account.
 */
export default function Transactions() {
  // TODO: Get the balance from the Redux store using the useSelector hook
  const balance = useSelector(state => state.transactions.balance);
  

  const [amountStr, setAmountStr] = useState("0.00");

  const dispatch = useDispatch()

  /** Dispatches a transaction action based on the form submission. */
  const onTransaction = (e) => {
    e.preventDefault();

    // This changes depending on which button the user clicked to submit the form.
    // It will be either "deposit", "withdraw", or "transfer".
    const action = e.nativeEvent.submitter.name;

    const amount = +amountStr;

    
    dispatch({type: action, amount: amount});
    // TODO: Dispatch the appropriate transaction action based on `action`
  };

  return (
    <section className="transactions container">
      <h2>Transactions</h2>
      <figure>
        <figcaption>Current Balance &nbsp;</figcaption>
        <strong>${balance.toFixed(2)}</strong>
      </figure>
      <form onSubmit={onTransaction}>
        <div className="form-row">
          <label>
            Amount
            <input
              type="number"
              inputMode="decimal"
              min={0}
              step="0.01"
              value={amountStr}
              onChange={(e) => setAmountStr(e.target.value)}
            />
          </label>
          <div>
            <button default name="deposit">
              Deposit
            </button>
            <button name="withdraw">Withdraw</button>
          </div>
        </div>
        <div className="form-row">
          <label>
            Transfer to
            <input type="text" placeholder="Recipient Name" name="recipient" />
          </label>
          <button name="transfer">Transfer</button>
        </div>
      </form>
    </section>
  );
}

When I run all of this code, everything remains blank. Nothing gets updated, so I am making the assumption that I have used useDispatch incorrectly.


Solution

  • You are using Javascript, so there's no extra setup required to use the useDispatch hook, const dispatch = useDispatch() is correct.

    Issues

    The issue is that you are dispatching actions that don't have any reducers that handle them. The code is manually dispatching actions of type "deposit", "withdraw", and "transfer" but the generated actions from the transactionsSlice will be prefixed with the slice's name, e.g. the generated withdrawal, deposit, and transfer actions will have types "transactions/withdrawal", "transactions/deposit", and "transactions/transfer" respectively.

    The additional issue is that all RTK generated actions have a payload property. The argument passed to the action creator is placed on the action.payload property, i.e. dispatch(deposit({ amount: 13.37 }) would generate the action object

    {
      type: "transactions/deposit",
      payload: {
        amount: 13.37,
      },
    }
    

    The code you have currently generates action object literals of the shape:

    {
      type: "deposit",
      amount: 13.37,
    }
    

    Even if the type value was correct, action.payload.amount would be undefined in the reducers.

    Solution Suggestions

    A trivial fix would be to manually prepend the "transactions"-prefix to the type, and correctly form the payload object.

    const onTransaction = (e) => {
      e.preventDefault();
    
      // This changes depending on which button the user clicked to submit the form.
      // It will be either "deposit", "withdrawal", or "transfer".
      const type = `transactions/${e.nativeEvent.submitter.name}`;
      const payload = { amount: +amountStr };
        
      dispatch({ type, payload });
    };
    
    ...
    
    <button default name="deposit">Deposit</button>
    <button name="withdrawal">Withdraw</button>
    <button name="transfer">Transfer</button>
    

    or

    import { deposit, withdrawal, transfer } from "./transactionsSlice";
    
    ...
    
    const onTransaction = (e) => {
      e.preventDefault();
    
      // This changes depending on which button the user clicked to submit the form.
      const type = e.nativeEvent.submitter.name;
      const payload = { amount: +amountStr };
     
      dispatch({ type, payload });
    };
    
    ...
    
    <button
      default
      name={deposit.type}    // "transactions/deposit"
    >
      Deposit
    </button>
    <button
      name={withdrawal.type} // "transactions/withdrawal"
    >
      Withdraw
    </button>
    <button
      name={transfer.type}   // "transactions/transfer"
    >
      Transfer
    </button>
    

    Generally though you'll not want to do this sort of manual action object creation and should dispatch the actual action creators.

    Example Implementation:

    import { deposit, withdrawal, transfer } from "./transactionsSlice";
    
    ...
    
    const onTransaction = (e) => {
      e.preventDefault();
    
      let action;
      const amount = +amountStr;
    
      switch(e.nativeEvent.submitter.name) {
        case "deposit":
          action = deposit;
          break;
    
        case "withdrawal":
          action = withdrawal;
          break;
    
        case "transfer":
          action = transfer;
          break;
    
        default:
          // don't do anything
      }
    
      if (action) {
        dispatch(action({ amount }));
      }
    };
    
    ...
    
    <button default name="deposit">Deposit</button>
    <button name="withdrawal">Withdraw</button>
    <button name="transfer">Transfer</button>