React keywords: applyMiddleware, combineReducers, composeWithDevTools, configureStore, connect, createCustomer, createSlice, createStore, Provider, thunk, useDispatch, useSelector,

Project overview:

Project shows: This project uses the Redux library instead of using the Context API with a reducer.

Store JS file versions:

store-v1.js: the initial version of ‘index.js’ contained import “./store-v1” which imported the entire file, and simply ran the ‘store-v1.js’ code inside of ‘index.js’. The store-v1.js used createStore from “redux” to expose a store.dispatch function which was used to alter the store state variable through action.type, just like in the useReducer hook:

index.js:

import React from “react”;
import ReactDOM from “react-dom/client”;

import store from “./store”;

const root = ReactDOM.createRoot(document.getElementById(“root”));
root.render(
  <React.StrictMode>
      <App />
  </React.StrictMode>
);

store-v1.js:

import { combineReducers, createStore } from “redux”;

const initialStateCustomer = {

  fullName: “”,
  nationalID: “”,
  createdAt: “”,
};
function customerReducer(state = initialStateCustomer, action) {
  switch (action.type) {
    case “customer/createCustomer”:
      return {
        state,
        fullName: action.payload.fullName,
        nationalID: action.payload.nationalID,
        createdAt: action.payload.createdAt,
      };
    …
  }
}
function accountReducer(state = initialStateAccount, action) {
switch (action.type) {
    case account/deposit”:
      return { state, balance: state.balance + action.payload };
    …
  }
}
const rootReducer = combineReducers({ – redux figures out what reducer to call based on the action.type specifiers
  account: accountReducer,
  customer: customerReducer,
});
const store = createStore(rootReducer);
store.dispatch({ type: “account/deposit”, payload: 500 });
store.dispatch(createCustomer(“Jonas Schmedtmann”, “24343434”));
function createCustomer(fullName, nationalID) { – an additional function is required because the reducer cannot directly change the state of the createdAt variable
  return {
    type: customer/createCustomer”,
    payload: { fullName, nationalID, createdAt: new Date().toISOString() },
  };
}

The displaying of data on the UI is not important here.

store-v2.js: the second version of ‘index.js’ contained import store from “./store-v2” because ‘store-v2.js’ exported it. The store was exposed to the App component much like a Context API is exposed to the App component as shown below:

index.js:
import React from “react”;
import ReactDOM from “react-dom/client”;
import { Provider } from “react-redux”;
import “./index.css”;
import App from “./App”;

 

import store from “./store”;

 

const root = ReactDOM.createRoot(document.getElementById(“root”));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);
store-v2.js: the reducers were moved from this file into their own separate files known as feature slices, 1 for the account and 1 for the customer.
import { applyMiddleware, combineReducers, createStore } from “redux”;
import thunk from “redux-thunk”;
import { composeWithDevTools } from “redux-devtools-extension”;

 

import accountReducer from “./features/accounts/accountSlice”;
import customerReducer from “./features/customers/customerSlice”;

 

const rootReducer = combineReducers({
  account: accountReducer,
  customer: customerReducer,
});

 

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

 

export default store;
The reducers, the initialState objects, and the custom functions were all moved to their respective slice files (see ‘customerSlice-v1.js’ and ‘accountSlice-v1.js’). Additional files used to initializeand manipulate the CreateCustomer/Customer/AccountOperations/BalanceDisplay components get access to the global state through the useSelector/useDispatch hooks.
Because the store cannot process asynchronous operations, such as a remote fetch for converting the deposit amount to the correct currency, middleware called thunk is used. In the ‘accountSlice-v1.js’ file the thunk requires that the external API fetch is encapsulated by a function which returns a dispatch to the reducer, as shown below:
export function deposit(amount, currency) {
  if (currency === “USD”) return { type: “account/deposit”, payload: amount };

   return async function (dispatch, getState) { – required by redux thunk

    dispatch({ type: “account/convertingCurrency” });

     const res = await fetch(
      `https://api.frankfurter.app/latest?amount=${amount}&from=${currency}&to=USD`
    );

    const data = await res.json();
    const converted = data.rates.USD;

     dispatch({ type: “account/deposit”, payload: converted });

  };
}
and this function can now be dispatched by the ‘AccountOperations.js’ JSX as follows:
function handleDeposit() {
    if (!depositAmount) return;

    dispatch(deposit(depositAmount, currency)); – the fetch is encapsulated by the deposit function thunk

    setDepositAmount(“”);
    setCurrency(“USD”);
  }
<button onClick={handleDeposit} disabled={isLoading}>
            {isLoading ? “Converting…” : `Deposit ${depositAmount}`}
</button>
Finally, the redux toolkit is used to reduce the size of the code, as apposed to when using a reducer, and most importantly, appears to allow the user to directly manipulate the global state variable (behind the scenes this mutable logic is converted back into immutable logic!). The createStore is deprecated and the toolkits configureStore version is used (see ‘store.js’), and the thunk are no longer needed in ‘store.js’ because the toolkit provides the thunk functionality to the ‘accountSlice.js’ file.. The ‘accountSlice.js’/’customerSlice.js’ reducers are replaced by the createSlice hook. This hook allows state variables to be changed directly, such as:
  const accountSlice = createSlice({
  name: “account”,
  initialState,
  reducers: {
    deposit(state, action) {
      state.balance += action.payload; – the state balance variable is changed directly
      state.isLoading = false;
    },
    withdraw(state, action) {
      state.balance -= action.payload;
    },
});

Code:

<!DOCTYPE html>
<html lang=“en”>
  <head>
    <meta charset=“utf-8” />
    <link rel=“icon” href=“%PUBLIC_URL%/favicon.ico” />
    <meta name=“viewport” content=“width=device-width, initial-scale=1” />
    <meta name=“theme-color” content=“#000000” />
    <meta
      name=“description”
      content=“Web site created using create-react-app”
    />
    <link rel=“apple-touch-icon” href=“%PUBLIC_URL%/logo192.png” />
    <!–
      manifest.json provides metadata used when your web app is installed on a
      user’s mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    –>
    <link rel=“manifest” href=“%PUBLIC_URL%/manifest.json” />
    <!–
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike “/favicon.ico” or “favicon.ico”, “%PUBLIC_URL%/favicon.ico” will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    –>
    <title>🏦 The React-Redux Bank ⚛️</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id=“root”></div>
    <!–
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    –>
  </body>
</html>
import CreateCustomer from “./features/customers/CreateCustomer”;
import Customer from “./features/customers/Customer”;
import AccountOperations from “./features/accounts/AccountOperations”;
import BalanceDisplay from “./features/accounts/BalanceDisplay”;
import { useSelector } from “react-redux”;

//import “./store-v1.js”;

//import store from “./store-v2.js”;

//store.dispatch({ type: “account/deposit”, payload: 250 });
//console.log(store.getState());

function App() {
  const fullName = useSelector((state) => state.customer.fullName);

  return (
    <div>
      <h1>🏦 The React-Redux Bank ⚛️</h1>
      {fullName === “” ? (
        <CreateCustomer />
      ) : (
        <>
          <Customer />
          <AccountOperations />
          <BalanceDisplay />
        </>
      )}
    </div>
  );
}

export default App;
import { configureStore } from “@reduxjs/toolkit”;

import accountReducer from “./features/accounts/accountSlice”;
import customerReducer from “./features/customers/customerSlice”;

const store = configureStore({
  reducer: {
    account: accountReducer,
    customer: customerReducer,
  },
});

export default store;
import { combineReducers, createStore } from “redux”;

const initialStateAccount = {
  balance: 0,
  loan: 0,
  loanPurpose: “”,
};

const initialStateCustomer = {
  fullName: “”,
  nationalID: “”,
  createdAt: “”,
};

function accountReducer(state = initialStateAccount, action) {
  switch (action.type) {
    case “account/deposit”:
      return { state, balance: state.balance + action.payload };
    case “account/withdraw”:
      return { state, balance: state.balance action.payload };
    case “account/requestLoan”:
      if (state.loan > 0) return state;
      // LATER
      return {
        state,
        loan: action.payload.amount,
        loanPurpose: action.payload.purpose,
        balance: state.balance + action.payload.amount,
      };
    case “account/payLoan”:
      return {
        state,
        loan: 0,
        loanPurpose: “”,
        balance: state.balance state.loan,
      };

    default:
      return state;
  }
}

function customerReducer(state = initialStateCustomer, action) {
  switch (action.type) {
    case “customer/createCustomer”:
      return {
        state,
        fullName: action.payload.fullName,
        nationalID: action.payload.nationalID,
        createdAt: action.payload.createdAt,
      };
    case “customer/updateName”:
      return { state, fullName: action.payload };
    default:
      return state;
  }
}

const rootReducer = combineReducers({
  account: accountReducer,
  customer: customerReducer,
});

const store = createStore(rootReducer);

// store.dispatch({ type: “account/deposit”, payload: 500 });
// store.dispatch({ type: “account/withdraw”, payload: 200 });
// console.log(store.getState());
// store.dispatch({
//   type: “account/requestLoan”,
//   payload: { amount: 1000, purpose: “Buy a car” },
// });
// console.log(store.getState());
// store.dispatch({ type: “account/payLoan” });
// console.log(store.getState());

// const ACOOUNT_DEPOSIT = “account/deposit”;

function deposit(amount) {
  return { type: “account/deposit”, payload: amount };
}

function withdraw(amount) {
  return { type: “account/withdraw”, payload: amount };
}

function requestLoan(amount, purpose) {
  return {
    type: “account/requestLoan”,
    payload: { amount, purpose },
  };
}

function payLoan() {
  return { type: “account/payLoan” };
}

store.dispatch(deposit(500));
store.dispatch(withdraw(200));
console.log(store.getState());

store.dispatch(requestLoan(1000, “Buy a cheap car”));
console.log(store.getState());
store.dispatch(payLoan());
console.log(store.getState());

function createCustomer(fullName, nationalID) {
  return {
    type: “customer/createCustomer”,
    payload: { fullName, nationalID, createdAt: new Date().toISOString() },
  };
}

function updateName(fullName) {
  return { type: “account/updateName”, payload: fullName };
}

store.dispatch(createCustomer(“Jonas Schmedtmann”, “24343434”));
store.dispatch(deposit(250));
console.log(store.getState());
import { applyMiddleware, combineReducers, createStore } from “redux”;
import thunk from “redux-thunk”;
import { composeWithDevTools } from “redux-devtools-extension”;

 

import accountReducer from “./features/accounts/accountSlice”;
import customerReducer from “./features/customers/customerSlice”;

 

const rootReducer = combineReducers({
  account: accountReducer,
  customer: customerReducer,
});

 

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

 

export default store;
import { useState } from “react”;
import { useDispatch, useSelector } from “react-redux”;
import { deposit, payLoan, requestLoan, withdraw } from “./accountSlice”;

function AccountOperations() {
  const [depositAmount, setDepositAmount] = useState(“”);
  const [withdrawalAmount, setWithdrawalAmount] = useState(“”);
  const [loanAmount, setLoanAmount] = useState(“”);
  const [loanPurpose, setLoanPurpose] = useState(“”);
  const [currency, setCurrency] = useState(“USD”);

  const dispatch = useDispatch();
  const {
    loan: currentLoan,
    loanPurpose: currentLoanPurpose,
    balance,
    isLoading,
  } = useSelector((store) => store.account);

  console.log(balance);

  function handleDeposit() {
    if (!depositAmount) return;

    dispatch(deposit(depositAmount, currency));
    setDepositAmount(“”);
    setCurrency(“USD”);
  }

  function handleWithdrawal() {
    if (!withdrawalAmount) return;
    dispatch(withdraw(withdrawalAmount));
    setWithdrawalAmount(“”);
  }

  function handleRequestLoan() {
    if (!loanAmount || !loanPurpose) return;
    dispatch(requestLoan(loanAmount, loanPurpose));
    setLoanAmount(“”);
    setLoanPurpose(“”);
  }

  function handlePayLoan() {
    dispatch(payLoan());
  }

  return (
    <div>
      <h2>Your account operations</h2>
      <div className=“inputs”>
        <div>
          <label>Deposit</label>
          <input
            type=“number”
            value={depositAmount}
            onChange={(e) => setDepositAmount(+e.target.value)}
          />
          <select
            value={currency}
            onChange={(e) => setCurrency(e.target.value)}
          >
            <option value=“USD”>US Dollar</option>
            <option value=“EUR”>Euro</option>
            <option value=“GBP”>British Pound</option>
          </select>

          <button onClick={handleDeposit} disabled={isLoading}>
            {isLoading ? “Converting…” : `Deposit ${depositAmount}`}
          </button>
        </div>

        <div>
          <label>Withdraw</label>
          <input
            type=“number”
            value={withdrawalAmount}
            onChange={(e) => setWithdrawalAmount(+e.target.value)}
          />
          <button onClick={handleWithdrawal}>
            Withdraw {withdrawalAmount}
          </button>
        </div>

        <div>
          <label>Request loan</label>
          <input
            type=“number”
            value={loanAmount}
            onChange={(e) => setLoanAmount(+e.target.value)}
            placeholder=“Loan amount”
          />
          <input
            value={loanPurpose}
            onChange={(e) => setLoanPurpose(e.target.value)}
            placeholder=“Loan purpose”
          />
          <button onClick={handleRequestLoan}>Request loan</button>
        </div>

        {currentLoan > 0 && (
          <div>
            <span>{`Pay back ${currentLoan} (${currentLoanPurpose}) `}</span>
            <button onClick={handlePayLoan}>Pay loan</button>
          </div>
        )}
      </div>
    </div>
  );
}

export default AccountOperations;
import { createSlice } from “@reduxjs/toolkit”;

 

const initialState = {
  balance: 0,
  loan: 0,
  loanPurpose: “”,
  isLoading: false,
};

 

const accountSlice = createSlice({
  name: “account”,
  initialState,
  reducers: {
    deposit(state, action) {
      state.balance += action.payload;
      state.isLoading = false;
    },
    withdraw(state, action) {
      state.balance -= action.payload;
    },
    requestLoan: {
      prepare(amount, purpose) {
        return {
          payload: { amount, purpose },
        };
      },

 

      reducer(state, action) {
        if (state.loan > 0) return;

 

        state.loan = action.payload.amount;
        state.loanPurpose = action.payload.purpose;
        state.balance = state.balance + action.payload.amount;
      },
    },
    payLoan(state) {
      state.balance -= state.loan;
      state.loan = 0;
      state.loanPurpose = “”;
    },
    convertingCurrency(state) {
      state.isLoading = true;
    },
  },
});

 

export const { withdraw, requestLoan, payLoan } = accountSlice.actions;

 

export function deposit(amount, currency) {
  if (currency === “USD”) return { type: “account/deposit”, payload: amount };

 

  return async function (dispatch, getState) {
    dispatch({ type: “account/convertingCurrency” });

 

    const res = await fetch(
      `https://api.frankfurter.app/latest?amount=${amount}&from=${currency}&to=USD`
    );
    const data = await res.json();
    const converted = data.rates.USD;

 

    dispatch({ type: “account/deposit”, payload: converted });
  };
}

 

export default accountSlice.reducer;
const initialState = {
  balance: 0,
  loan: 0,
  loanPurpose: “”,
  isLoading: false,
};

 

export default function accountReducer(state = initialState, action) {
  switch (action.type) {
    case “account/deposit”:
      return {
        …state,
        balance: state.balance + action.payload,
        isLoading: false,
      };
    case “account/withdraw”:
      return { …state, balance: state.balance – action.payload };
    case “account/requestLoan”:
      if (state.loan > 0) return state;
      // LATER
      return {
        …state,
        loan: action.payload.amount,
        loanPurpose: action.payload.purpose,
        balance: state.balance + action.payload.amount,
      };
    case “account/payLoan”:
      return {
        …state,
        loan: 0,
        loanPurpose: “”,
        balance: state.balance – state.loan,
      };
    case “account/convertingCurrency”:
      return { …state, isLoading: true };

 

    default:
      return state;
  }
}

 

export function deposit(amount, currency) {
  if (currency === “USD”) return { type: “account/deposit”, payload: amount };

 

  return async function (dispatch, getState) {
    dispatch({ type: “account/convertingCurrency” });

 

    const res = await fetch(
      `https://api.frankfurter.app/latest?amount=${amount}&from=${currency}&to=USD`
    );
    const data = await res.json();
    const converted = data.rates.USD;

 

    dispatch({ type: “account/deposit”, payload: converted });
  };
}

 

export function withdraw(amount) {
  return { type: “account/withdraw”, payload: amount };
}

 

export function requestLoan(amount, purpose) {
  return {
    type: “account/requestLoan”,
    payload: { amount, purpose },
  };
}

 

export function payLoan() {
  return { type: “account/payLoan” };
}
import { connect } from “react-redux”;

 

function formatCurrency(value) {
  return new Intl.NumberFormat(“en”, {
    style: “currency”,
    currency: “USD”,
  }).format(value);
}

 

function BalanceDisplay({ balance }) {
  return <div className=“balance”>{formatCurrency(balance)}</div>;
}

 

function mapStateToProps(state) {
  return {
    balance: state.account.balance,
  };
}

 

export default connect(mapStateToProps)(BalanceDisplay); – the connect() function connects a React component to a Redux store
import { useState } from “react”;
import { useDispatch } from “react-redux”;
import { createCustomer } from “./customerSlice”;

function Customer() {
  const [fullName, setFullName] = useState(“”);
  const [nationalId, setNationalId] = useState(“”);

  const dispatch = useDispatch();

  function handleClick() {
    if (!fullName || !nationalId) return;
    dispatch(createCustomer(fullName, nationalId));
  }

  return (
    <div>
      <h2>Create new customer</h2>
      <div className=“inputs”>
        <div>
          <label>Customer full name</label>
          <input
            value={fullName}
            onChange={(e) => setFullName(e.target.value)}
          />
        </div>
        <div>
          <label>National ID</label>
          <input
            value={nationalId}
            onChange={(e) => setNationalId(e.target.value)}
          />
        </div>
        <button onClick={handleClick}>Create new customer</button>
      </div>
    </div>
  );
}

export default Customer;
import { useSelector } from “react-redux”;

function Customer() {
  const customer = useSelector((store) => store.customer.fullName);

  console.log(customer);

  return <h2>👋 Welcome, {customer}</h2>;
}

export default Customer;
import { createSlice } from “@reduxjs/toolkit”;

 

const initialState = {
  fullName: “”,
  nationalID: “”,
  createdAt: “”,
};

 

const customerSlice = createSlice({
  name: “customer”,
  initialState,
  reducers: {
    createCustomer: {
      prepare(fullName, nationalID) {
        return {
          payload: {
            fullName,
            nationalID,
            createdAt: new Date().toISOString(),
          },
        };
      },
      reducer(state, action) {
        state.fullName = action.payload.fullName;
        state.nationalID = action.payload.nationalID;
        state.createdAt = action.payload.createdAt;
      },
    },
    updateName(state, action) {
      state.fullName = action.payload;
    },
  },
});

 

export const { createCustomer, updateName } = customerSlice.actions;

 

export default customerSlice.reducer;
const initialState = {
  fullName: “”,
  nationalID: “”,
  createdAt: “”,
};

export default function customerReducer(state = initialState, action) {

  switch (action.type) {
    case “customer/createCustomer”:
      return {
        …state,
        fullName: action.payload.fullName,
        nationalID: action.payload.nationalID,
        createdAt: action.payload.createdAt,
      };
    case “customer/updateName”:
      return { …state, fullName: action.payload };
    default:
      return state;
  }
}

 

export function createCustomer(fullName, nationalID) {
  return {
    type: “customer/createCustomer”,
    payload: { fullName, nationalID, createdAt: new Date().toISOString() },
  };
}

 

export function updateName(fullName) {
  return { type: “customer/updateName”, payload: fullName };
}
import React from “react”;
import ReactDOM from “react-dom/client”;
import { Provider } from “react-redux”;
import “./index.css”;
import App from “./App”;

 

import store from “./store”;

 

const root = ReactDOM.createRoot(document.getElementById(“root”));
root.render(
  <React.StrictMode>
    <Provider store={store}> – broadcasting the store global state into any component that wants it
      <App />
    </Provider>
  </React.StrictMode>
);
body {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto,
    Oxygen, Ubuntu, Cantarell, ‘Open Sans’, ‘Helvetica Neue’, sans-serif;
  margin: 40px;
}

input,
select {
  margin: 0 8px;
  padding: 4px 8px;
  font: inherit;
}

button {
  text-transform: uppercase;
  font-weight: bold;
  padding: 6px 8px;
  cursor: pointer;
}

.inputs {
  background-color: #f7f7f7;
  padding: 32px;
}

.inputs > * + * {
  margin-top: 20px;
}

.balance {
  position: absolute;
  top: 40px;
  right: 40px;
  background-color: #f7f7f7;
  padding: 24px 32px;
  font-weight: bold;
  font-size: 32px;
  min-width: 180px;
  text-align: center;
}