Replacing React Hooks with React Redux State Management Library Part 3

Replacing Our State Hooks

In the previous chapter, I discussed the current state of the application, the underlying architecture, how the internal components(specifically the form that handles currency input) functioned and what direction the application needed to go.

This part focuses on how the state that was lifted from the Body component to the App component functions(a quick run down) before replacing the state hooks with the React Redux library. But first;

What is Redux?

According to the Redux Docs:

Redux is a predictable state container for JavaScript applications that helps us write applications that behave consistently, run in different environments, and are easy to test.

Redux also provides several benefits, which according to the Redux Docs are:

  • Predictable

  • Centralized

  • Debuggable

  • Flexible

Predictable

Redux helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test.

Centralized

Centralizing your application's state and logic enables powerful capabilities like undo/redo, state persistence, and much more.

Debuggable

The Redux DevTools make it easy to trace when, where, why, and how your application's state changed. Redux's architecture lets you log changes, use "time-travel debugging", and even send complete error reports to a server.

Flexible

Redux works with any UI layer and has a large ecosystem of add-ons to fit your needs.

These benefits are points listed in the Redux Docs.

Our App State

Now that we've discussed what Redux is and what we can gain by using the library, we'll go over the logic in our App component:

Starting from the top:

import React, { useState, useEffect, useCallback } from "react"
import LoadingState from "./util/LoadingState/LoadingState"
import Footer from "./components/Footer/Footer"
const Body = React.lazy(() => import("./components/Body/Body"))
const Header = React.lazy(() => import("./components/Header/Header"))
const Nav = React.lazy(() => import("./components/Navigation/Nav"))

We're importing the React library and our state hooks useState, useEffect and useCallback , we're also importing a LoadingState React component which would serve as our fallback for lazy loading our React components then finally a Footer component. You can read about lazy loading React components on:

React Dev

The main App component has several state variables created through the useState hook.

function App() {

  const [swap, setSwap] = useState(true)
  const [defaultNumber, setDefaultNumber] = useState(0)
  const [convertedNumber, setConvertedNumber] = useState(null)
  const [currencyRate, setCurrencyRate] = useState(null)
  const [currency, setCurrency] = useState({
    defaultType: 'USD',
    defaultTypeAmount: 0,
    convertedType: 'NGN',
    convertedTypeAmount: 0
  })
....
}

The swap state variable along with its updater function setSwap are for swapping the place of the currency state object between currency.defaultType with its currency.defaultTypeAmount against currency.convertedType and currency.convertedTypeAmount state object properties. This just means that the currency object is tracking 4 object keys that represent a currency pair along with a setCurrency updater function.

We also have the convertedNumber and currencyRate which currently are for calculating the newly converted amount of currency along with calculating the rate of the in-memory currency pair.

Below this part of our code:

useEffect(() => {
    if (!currencyRate) {
      fetch('../data/currencyData.json').then((response) => {
        return response.json()
      }).then((data) => {
        for (const rate of data) {
          if (rate["exchange_rate"]) {
          setCurrencyRate(Math.floor(rate["exchange_rate"]))
        }
        }
      }).catch((error) => console.error(error))
    }
  }, [currency.convertedTypeAmount, currencyRate])

  useEffect(() => {
    if (swap) {
      setConvertedNumber(Number(currency.defaultTypeAmount * currencyRate))
    }
    if (currency.convertedTypeAmount) {
      setDefaultNumber(Number(currency.convertedTypeAmount / currencyRate))
    }
  }, [convertedNumber, currency.defaultAmount, currency.defaultTypeAmount, currency.convertedTypeAmount, currencyRate, swap])

The first useEffect hook is for mocking an api call to the in-memory data(currencyData.json) and then setting the currency rate through the setCurrencyRate updater function, this will give the currency a value on load because we first checked if the !currencyRate isn't available, since we set the initial currencyRate to null this would trigger the fetch function to make the GET request and get our in-memory currency pair data. It's this data that's set by the setCurrency.

The next useEffect will check if swap is true and set the convertedNumber to it's supposed value by multiplying the currencyRate we get back from the setCurrency updater function and if swap is not true we do the conversion of the defaultNumber by dividing the converted currency by the currency rate.

Next:

const changeCurrencyHandler = (e) => {
    const name = e.target.name
    const value = e.target.value
    const type = e.target.name === "defaultTypeAmount" ? currency.defaultType : currency.convertedType
    setCurrency({ ...currency, [name]: Number(value.toLocaleString("en-US", { style: "currency", currency: type})) })
  }

  const swapCurrencies = useCallback(() => {
    const defaultTypes = currency.defaultType
    const defaultAmounts = currency.defaultTypeAmount
    const convertedTypes = currency.convertedType
    const convertedTypeAmounts = currency.convertedTypeAmount

    setSwap(!swap)
    setCurrency({
        ...currency, 
        defaultType: convertedTypes, 
        defaultTypeAmount: convertedTypeAmounts, 
        convertedType: defaultTypes, 
        convertedTypeAmount: defaultAmounts
    })
  }, [currency, swap])

We have the changeCurrencyHandler function above along with the swapCurrencies function that is for swapping the defaultCurrency value with the convertedCurrency value. The changeCurrencyHandler is for handling input, so it is triggered by the onChange attribute of the currency input element below:

    // ConvertedCurrency.jsx

import {
    CurrencyInput,
} from '../../styles/BodyUtil'

const ConvertedCurrency = ({currency, changeCurrencyHandler}) => {
    return (
        <CurrencyInput type="number" name="convertedTypeAmount" id="convertedTypeAmount"
        value={currency.convertedTypeAmount || ""}
        placeholder="0"
        onChange={changeCurrencyHandler}
        min={0}
        max={999999}
        step={0.5}
        />
    )
}

export default ConvertedCurrency

// DefaultCurrency.jsx

import {
    CurrencyInput
} from '../../styles/BodyUtil'

const DefaultCurrency = ({currency, changeCurrencyHandler}) => {
    return (
        <CurrencyInput type="number" name="defaultTypeAmount" id="defaultTypeAmount" 
                value={currency.defaultTypeAmount || ""}
                placeholder="0"
                onChange={changeCurrencyHandler}
                min={0}
                max={999999}
                step={0.5}
                />
    )
}

export default DefaultCurrency

The input elements for the ConvertedCurrency and DefaultCurrency components fundamentally do the same thing, accept the currency and changeCurrencyHandler function through object destructuring and then use the currency state variable to determine the type of currency and the format the currency should be displayed in, there are also attributes to restrict the range of the currency so the user interface of the input element does not exceed the screen or "overflow".

Finally, we have the user interface that is returned from the App component:

.... // imported hooks and functions i listed earlier

function App() {
... // handlers we've gone through
  return (
      <React.Suspense fallback={<LoadingState />}>
    <div>
    <Nav />
    <main>
    <Header />
    <Body 
    currency={currency}
    convertedNumber={convertedNumber}
    swapCurrencies={swapCurrencies} 
    changeCurrencyHandler={changeCurrencyHandler}
    defaultNumber={defaultNumber}
    />
    <Footer />
    </main>
    </div>
    </React.Suspense>
  )
}

export default App

So we are returning a lazy-loaded component that holds the Nav, Header and the Body(which holds the form), this means as we navigate either to or away from the main page there would be an intermission where the fallback LoadingState component would be shown, you can also see we're passing the state variables currency, convertedNumber, the handlers swapCurrencies and changeCurrencyHandler along with the defaultNumber variable.

Now that we've gone through the App component, it's state variables and handlers, time to implement Redux and load live data.

Implementing the Redux library

In part 2 of this series we discussed about the Redux library, and how it allowed developers to create a state store and dispatch actions and handle side effects through reducers. Now, we'll be discussing on what we need from Redux in our project. Since we'll be making real API calls, we'll be using Redux asynchronously so we'll be using Redux thunk in that area. Here are the steps we'll be taking:

  • Installing Redux and setting up a Redux store

  • Creating our Reducer(s) and action objects

  • Devise how our REST API should be integrated with our reducers

  • Extract state and dispatch to our User Interface

Installing Redux and setting up a Redux store

Let's install react-redux, redux-thunk and redux, enter into the terminal:

npm install react-redux --save
npm install redux --save
npm install redux-thunk

You should get a response similar to this after installation:

Great, the packages are installed.

Time to set up the Redux store, we'll go to the main.jsx file:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import ErrorBoundary from './ErrorBoundary.jsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <ErrorBoundary> // the error boundary we created in part 2 of this series
    <App />
    </ErrorBoundary>
  </React.StrictMode>,
)

This is where we'll import the legacy_createStore method so we can create a global state store, the legacy_createStore method accepts two parameters or arguments, the first is the reducer function and the second is an optional middleware or dev tool(not optional for this project as we would need the thunk middleware and dev tool).

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import ErrorBoundary from './ErrorBoundary.jsx'
import './index.css'
import {legacy_createStore, applyMiddleware, compose} from "redux"
import thunk from "redux-thunk"
import {Provider} from "react-redux"
import currencyReducer from './util/ReduxReducers/currencyReducer/currencyReducer.js'

const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

const store = legacy_createStore(currencyReducer, composeEnhancer(applyMiddleware(thunk)))

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}>
    <ErrorBoundary>
    <App />
    </ErrorBoundary>
    </Provider>
  </React.StrictMode>,
)

With a bit of help from Stackoverflow we've set up the Redux devtools. This gives us access to the time-travel debugging feature Redux has.

Creating our Reducer(s) and action objects

We've created a reducer function that is exported from a currencyReducer.js file which is stored in a ReduxReducers folder inside of our util folder(breaking up the main state from the rest of our application).

// currencyReducer.js
const READY = "READY"
const FETCH = "FETCH"
const RESET = "RESET"
const CHANGE = "CHANGE"

const initialState = {
    old_currency : "USD",
    old_amount: 0,
    new_currency: "NGN",
    new_amount: 0
}

const currencyReducer = (state = initialState, action) => {
    console.log(state)
    switch(action.type) {
        case READY:
        return state;

        case FETCH:
        return state;

        case RESET:
        return state;

        case CHANGE:
        return {...state, ...action};

        default:
        return state;       
    }
}

export default currencyReducer;

NOTE: This is just a rough guestimate as to what the reducer would be like. It is modeled after the data the API provides, which follows the same structure as the initialState.

Devise how our REST API should be integrated with our reducers

Our Currency Converter API is gotten from the "Convert Currency" API from api-ninjas. The returned JSON data is modelled below:

{
  "new_amount": 3838849.02,
  "new_currency": "NGN",
  "old_currency": "USD",
  "old_amount": 5000
}

As you can see the JSON data above matches the scheme of our initialState object. We are also provided a table of over a 100 currencies.

Currency API Docs

Currency SymbolCurrency NameCurrency SymbolCurrency Name
AEDUAE DirhamAFNAfghan Afghani
ALLAlbanian LekAMDArmenian Dram
ANGNetherlands Antillean GuldenAOAAngolan Kwanza
ARSArgentine PesoAUDAustralian Dollar
AWGAruban FlorinAZNAzerbaijani Manat
BAMBosnia And Herzegovina Konvertibilna MarkaBBDBarbadian Dollar
BDTBangladeshi TakaBGNBulgarian Lev
BHDBahraini DinarBIFBurundi Franc
BMDBermudan DollarBNDBrunei Dollar
BOBBolivian BolivianoBRLBrazilian Real
BSDBahamian DollarBTCBitcoin
BTNBhutanese NgultrumBWPBotswana Pula
.............

Wow, that's quite some data!

We are going to map over the page and return all currency symbols to our developer console on the site:

// IN OUR BROWSERS DEVELOPER CONSOLE ON API-NINJAS CURRENCY CONVERTER PAGE

/* select the table header rows, they're a Node List */
let rows = document.querySelectorAll("th[scope='row']") 

/* loop through the Node List to return only the symbol and names and
save symbols to array */
let currencySymbol = []

for (const data of rows) {
currencySymbol.push(data.innerText)
}

let symbols = currencySymbol.join("_") // creates a string of currency symbols separated by a _.

This gives us all the currency symbols in the API, copy the data in the symbols variable. Create a currencySymbol.js file in a currencySymbol folder under the util folder and store the data.

// currencySymbol.js

let symbols = 'AED_AFN_ALL_AMD_ANG_AOA_ARS_AUD_AWG_AZN_BAM_BBD_BDT_BGN_BHD_BIF_BMD_BND_BOB_BRL_BSD_BTC_BTN_BWP_BYN_BYR_BZD_CAD_CDF_CHF_CLF_CLP_CNY_COP_CRC_CUC_CUP_CVE_CZK_DJF_DKK_DOP_DZD_EGP_ERN_ETB_EUR_FJD_FKP_GBP_GEL_GGP_GHS_GIP_GMD_GNF_GTQ_GYD_HKD_HNL_HRK_HTG_HUF_IDR_ILS_IMP_INR_IQD_IRR_ISK_JEP_JMD_JOD_JPY_KES_KGS_KHR_KMF_KPW_KRW_KWD_KYD_KZT_LAK_LBP_LKR_LRD_LSL_LVL_LYD_MAD_MDL_MGA_MKD_MMK_MNT_MOP_MRO_MUR_MVR_MWK_MXN_MYR_MZN_NAD_NGN_NIO_NOK_NPR_NZD_OMR_PAB_PEN_PGK_PHP_PKR_PLN_PYG_QAR_RON_RSD_RUB_RWF_SAR_SBD_SCR_SDG_SEK_SGD_SHP_SLL_SOS_SRD_STD_SVC_SYP_SZL_THB_TJS_TMT_TND_TOP_TRY_TTD_TWD_TZS_UAH_UGX_USD_UYU_UZS_VEF_VND_VUV_WST_XAF_XAG_XCD_XDR_XOF_XPF_YER_ZAR_ZMK_ZMW_ZWL'

let currencySymbols = symbols.split("_"); // this will split the symbols string into an array of symbols

export default currencySymbols;

We can also extract the names of the currencies.

let namesOfCurrencies = 'UAE Dirham_Afghan Afghani_Albanian Lek_Armenian Dram_Netherlands Antillean Gulden_Angolan Kwanza_Argentine Peso_Australian Dollar_Aruban Florin_Azerbaijani Manat_Bosnia And Herzegovina Konvertibilna Marka_Barbadian Dollar_Bangladeshi Taka_Bulgarian Lev_Bahraini Dinar_Burundi Franc_Bermudan Dollar_Brunei Dollar_Bolivian Boliviano_Brazilian Real_Bahamian Dollar_Bitcoin_Bhutanese Ngultrum_Botswana Pula_New Belarusian Ruble_Belarusian Ruble_Belize Dollar_Canadian Dollar_Congolese Franc_Swiss Franc_Chilean Unit Of Account_Chilean Peso_Chinese Yuan_Colombian Peso_Costa Rican Colon_Cuban Convertible Peso_Cuban Peso_Cape Verdean Escudo_Czech Koruna_Djiboutian Franc_Danish Krone_Dominican Peso_Algerian Dinar_Egyptian Pound_Eritrean Nakfa_Ethiopian Birr_Euro_Fijian Dollar_Falkland Islands Pound_British Pound_Georgian Lari_Guernsey Pound_Ghanaian Cedi_Gibraltar Pound_Gambian Dalasi_Guinean Franc_Guatemalan Quetzal_Guyanese Dollar_Hong Kong Dollar_Honduran Lempira_Croatian Kuna_Haitian Gourde_Hungarian Forint_Indonesian Rupiah_Israeli New Sheqel_Manx pound_Indian Rupee_Iraqi Dinar_Iranian Rial_Icelandic Króna_Jersey Pound_Jamaican Dollar_Jordanian Dinar_Japanese Yen_Kenyan Shilling_Kyrgyzstani Som_Cambodian Riel_Comorian Franc_North Korean Won_South Korean Won_Kuwaiti Dinar_Cayman Islands Dollar_Kazakhstani Tenge_Lao Kip_Lebanese Lira_Sri Lankan Rupee_Liberian Dollar_Lesotho Loti_Latvian Lats_Libyan Dinar_Moroccan Dirham_Moldovan Leu_Malagasy Ariary_Macedonian Denar_Myanma Kyat_Mongolian Tugrik_Macanese Pataca_Mauritanian Ouguiya_Mauritian Rupee_Maldivian Rufiyaa_Malawian Kwacha_Mexican Peso_Malaysian Ringgit_Mozambican Metical_Namibian Dollar_Nigerian Naira_Nicaraguan Cordoba_Norwegian Krone_Nepalese Rupee_New Zealand Dollar_Omani Rial_Panamanian Balboa_Peruvian Nuevo Sol_Papua New Guinean Kina_Philippine Peso_Pakistani Rupee_Polish Zloty_Paraguayan Guarani_Qatari Riyal_Romanian Leu_Serbian Dinar_Russian Ruble_Rwandan Franc_Saudi Riyal_Solomon Islands Dollar_Seychellois Rupee_Sudanese Pound_Swedish Krona_Singapore Dollar_Saint Helena Pound_Sierra Leonean Leone_Somali Shilling_Surinamese Dollar_Sao Tome And Principe Dobra_Salvadoran Colón_Syrian Pound_Swazi Lilangeni_Thai Baht_Tajikistani Somoni_Turkmenistan Manat_Tunisian Dinar_Paanga_Turkish New Lira_Trinidad and Tobago Dollar_New Taiwan Dollar_Tanzanian Shilling_Ukrainian Hryvnia_Ugandan Shilling_United States Dollar_Uruguayan Peso_Uzbekistani Som_Venezuelan Bolivar_Vietnamese Dong_Vanuatu Vatu_Samoan Tala_Central African CFA Franc_Silver (troy ounce)_East Caribbean Dollar_Special Drawing Rights_West African CFA Franc_CFP Franc_Yemeni Rial'

let currencyNames = namesOfCurrencies.split("_") // splits the currencyNames into an array

export default currencyNames

Wonderful, we have the names of all our currencies including the symbols, lets test it out in our option component in our form:

// ... After importing all necessary files
import currencySymbols from '../../util/CurrencySymbols/currencySymbols'
import currencyNames from '../../util/CurrencyNames/currencyNames'

function Body({currency, swapCurrencies, changeCurrencyHandler, convertedNumber, defaultNumber}) {

    const oldCurrency = useSelector((state) => state.old_currency)
    const oldAmount = useSelector((state) => state.old_amount)
    const newCurrency = useSelector((state) => state.new_currency)
    const newAmount = useSelector((state) => state.new_amount)
    const dispatch = useDispatch()

    function selectCurrency(e) {
        const value = e.target.value
        const name = e.target.name
        dispatch({ type: "CHANGE", [name]: value})
    }

    return (
        <FormElement>
                <Legend>Swap and Compare Currency</Legend>
            <FirstFieldSet>
                <DefaultLabel 
                defaultNumber={defaultNumber} 
                currency={currency} />
                <DefaultCurrency 
                currency={currency} 
                changeCurrencyHandler={changeCurrencyHandler} />
            </FirstFieldSet>
            <SecondFieldSet>
                <span style={{display: "flex", width: "max-content", margin: "auto", gap: "15px"}}>
                <Select value={oldCurrency} onChange={selectCurrency} name="old_currency">
                        {currencySymbols.map((symbol, idx) => (
                            <Option key={symbol} title={currencyNames[idx]} value={symbol}>{symbol}</Option>
                        ))}
                </Select>
                <Select value={newCurrency} onChange={selectCurrency} name="new_currency">
                        {currencySymbols.map((symbol, idx) => (
                            <Option key={symbol} title={currencyNames[idx]} value={symbol}>{symbol}</Option>
                        ))}
                </Select>
                </span>
                <SwapButton 
                currency={currency} 
                swapCurrencies={swapCurrencies} />
                <DefaultResetButton />
            </SecondFieldSet>
            <ThirdFieldSet>
                <ConvertedAmountLabel 
                convertedNumber={convertedNumber} 
                currency={currency} />
                <ConvertedCurrency 
                currency={currency} 
                changeCurrencyHandler={changeCurrencyHandler} />
            </ThirdFieldSet>
        </FormElement>
    )
}


export default Body

Great! We're doing a couple of things here, we're importing the currencySymbols array to our select element and then we map each currency in it into an option element while also giving each option element a title attribute and then passing the currencyNames array to it and selecting the index of each currency being viewed with idx.

Extract State and Dispatch to our User Interface

Now that Redux is set up, we clear the application of all previous states that aren't relevant to Redux.

Then begin integrating our state into the components, starting from the Body component. In the Body component we send the oldCurrency and newCurrency to the SwapButton component.

// previos styles,

import currencySymbols from 
'../../util/CurrencySymbols/currencySymbols'
import { useSelector, useDispatch } from "react-redux"

function Body() {

    const oldCurrency = useSelector((state) => state.old_currency)
    const oldAmount = useSelector((state) => state.old_amount)
    const newCurrency = useSelector((state) => state.new_currency)
    const newAmount = useSelector((state) => state.new_amount)
    const dispatch = useDispatch()

    function selectCurrency(e) {
        const value = e.target.value
        const name = e.target.name
        dispatch({ type: "CHANGE", [name]: value})
    }

    return (
        <FormElement>
                <Legend>Swap and Compare Currency</Legend>
            <FirstFieldSet>
                <DefaultLabel />
                <DefaultCurrency />
            </FirstFieldSet>
            <SecondFieldSet>
                <Span>
                <Select value={oldCurrency} onChange={selectCurrency} name="old_currency">
                        {currencySymbols.map((symbol) => (
                            <Option key={symbol} value={symbol}>{symbol}</Option>
                        ))}
                </Select>
                <Select value={newCurrency} onChange={selectCurrency} name="new_currency">
                        {currencySymbols.map((symbol) => (
                            <Option key={symbol} value={symbol}>{symbol}</Option>
                        ))}
                </Select>
                </Span>
                <SwapButton 
                oldCurrency={oldCurrency}
                newCurrency={newCurrency}
                <DefaultResetButton />
            </SecondFieldSet>
            <ThirdFieldSet>
                <ConvertedAmountLabel />
                <ConvertedCurrency />
            </ThirdFieldSet>
        </FormElement>
    )
}

export default Body

We then destructure and use the state to display the button.

We also send the oldAmount("USD default value") to the DefaultCurrency component, we import useDispatch and then create a handleInput function to make the input a controlled input that updates the state through dispatch.

import {
    CurrencyInput
} from '../../styles/BodyUtil'
import { useDispatch } from "react-redux"

const DefaultCurrency = ({oldAmount}) => {

    const dispatch = useDispatch()

    const handleInput = (e) => {
        const name = e.target.name
        const value = Number(e.target.value) || 0
        dispatch({type: "CHANGE", [name]: value})
    }

    return (
        <CurrencyInput type="number" name="old_amount" id="defaultTypeAmount" 
                value={oldAmount || ""}
                placeholder="0"
                onChange={handleInput} 
                min={0}
                max={999999}
                step={0.5}
                />
    )
}

export default DefaultCurrency

We do the same for the convertedCurrency but using the newCurrency.

import {
    CurrencyInput,
} from '../../styles/BodyUtil'
import { useDispatch } from "react-redux"

const ConvertedCurrency = ({newAmount}) => {

    const dispatch = useDispatch()

    const handleInput = (e) => {
        const name = e.target.name
        const value = Number(e.target.value) || 0
        dispatch({type: "CHANGE", [name]: value})
    }

    return (
        <CurrencyInput type="number" name="new_amount" id="convertedTypeAmount"
        value={newAmount || ""}
        placeholder="0"
        onChange={handleInput}
        min={0}
        max={999999}
        step={0.5}
        />
    )
}

export default ConvertedCurrency

Now that we've controlled the inputs in our input elements we can work on the logic to fetch our currency from the api.

We create a fetchCurrency.js file in a fetchCurrency folder under our utils folder.

// fetchCurrency.js

export const fetchCurrency = (value, oldCurrency, newCurrency) => async dispatch => {
    if (value === 0) return
    console.log(oldCurrency, newCurrency, value)
        const data = await fetch(`https://api.api-ninjas.com/v1/convertcurrency?want=${oldCurrency}&have=${newCurrency}&amount=${value}`, {
            method: 'get',
            headers: {
              'X-Api-Key': `${import.meta.env.VITE_API_KEY}`
          },
          })
        const response = await data.json()
        console.log(response)
        dispatch({type: "FETCH", ...response})
}

In the file we create a fetchCurrency function that accepts 3 parameters, value, oldCurrency and newCurrency , we make use of redux thunk for wait for our fetch request asynchronously while for each currency input we use the lodash debounce library to debounce requests so we don't make requests too fast.

/* eslint-disable react/prop-types */
/* ... styles and others */
import debounce from 'lodash.debounce'

const DefaultCurrency = ({oldAmount, oldCurrency, newCurrency}) => {

    const dispatch = useDispatch()

    const callForData = debounce((value) => {
      dispatch(fetchCurrency(value, oldCurrency, newCurrency))
    }, 700)

    const handleInput = (e) => {
        const name = e.target.name
        const value = Number(e.target.value) || 0
        dispatch({type: "CHANGE", [name]: value})
        callForData(value)
    }

    return (
        <CurrencyInput type="number" name="old_amount" id="defaultTypeAmount" 
                value={oldAmount || ""}
                placeholder="0"
                onChange={handleInput} 
                min={0}
                max={999999}
                step={0.5}
                />
    )
}

export default DefaultCurrency

But there's a problem here.

If both the default currency input and converted currency input have their values set to our Redux state variables of old_amount and new_amount then everytime we make API requests we'd only be updating the converted currency amount inside of the converted currency input.

This is a terrible User Experience. Instead, the best approach would probably be to add a boolean in our code that swaps which input element serves as the default. Normally, this would've been the solution, but since we're using Redux as a state store, we can just create a swap action that swaps our states., then dispatch this action anytime we need it.

const FETCH = "FETCH"
const RESET = "RESET"
const CHANGE = "CHANGE"
const SWAP = "SWAP"

const initialState = {
    old_currency : "USD",
    old_amount: 0,
    new_currency: "NGN",
    new_amount: 0,
}

const currencyReducer = (state = initialState, action) => {
    switch(action.type) {

        case FETCH:
        return {
            ...state, 
            ...action,
        };

        case RESET:
        return {
            ...initialState
        };

        case CHANGE:
        return {
            ...state, 
            ...action,
        };

        case SWAP:
        return {
            ...state, 
            old_currency: state.new_currency, 
            old_amount: state.new_amount,  
            new_currency: state.old_currency,
            new_amount: state.old_amount,
        }

        default:
        return state;
    }
}

export default currencyReducer

Great! Now we can swap the currencies. But things are getting crowded in the currencyReducer so we'll create a payloadReducer to handle the incoming data from the API.

// payloadReducer.js

const LOADING = "LOADING"
const FETCH_CONVERTED = "FETCH_CONVERTED"
const FETCH_DEFAULT = "FETCH_DEFAULT"
const RESET = "RESET"
const CONVERT = "CONVERT"
const RESOLVED = "RESOLVED"
const UNCONVERT = "UNCONVERT"

const initialState = {
    loading: null,
    payload_amount: 0,
    payload_currency: "",
    converted: false,
}

const payloadReducer = (state = initialState, action) => {
    switch(action.type) {
        case LOADING: // whenever input is given, we dispatch this action
        return {
            ...state, 
            loading: true,
        }

        case CONVERT: //whenever the converted currency input is used we dispatch this action
        return {
            ...state, converted: true,
        }
        case UNCONVERT: //whenever the default currecny input is used we dispatch this action
        return {
            ...state, 
            converted: false,
        }
        case RESOLVED: // whenever we get a successful response from api, change loading to false
        return {
            ...state,
            loading: false,
        }
        case FETCH_CONVERTED: // fetch currencies given to convert currency field
        return {
            ...state, 
            converted: true, 
            payload_amount: action.amount, 
            payload_currency: action.currency,
        }
        case FETCH_DEFAULT: // fetch currencies given to default field
        return {
            ...state, 
            converted: false,
            payload_amount: action.amount, 
            payload_currency: action.currency,
        }
        case RESET: // reset the both currency states to the default initial state
        return {
            ...initialState
        }
        default:
        return state
    }
}

export default payloadReducer

After creating these cases in the payloadReducer that will handle our fetching of currency data from input we set the fetchCurrency function to handle these cases.

export const fetchCurrency = (value, oldCurrency, newCurrency, converted) => async dispatch => {

        const data = await fetch(`https://api.api-ninjas.com/v1/convertcurrency?want=${newCurrency}&have=${oldCurrency}&amount=${value}`, {
            method: 'get',
            headers: {
              'X-Api-Key': `${import.meta.env.VITE_API_KEY}` // importing my api key
          },
          })

        const response = await data.json()

        console.log(response)

        if (response) {
          dispatch({type: "RESOLVED"}) // changes the loading state to false
          switch(converted) {
            case true:
            return dispatch({
              type: "FETCH_CONVERTED", // gets the new currency when converted currency input is active
              amount: response.new_amount,
              currency: response.new_currency
              })
            case false:
            return dispatch({
              type: "FETCH_DEFAULT", // gets the new currency when the default currency field is active
              amount: response.new_amount,
              currency: response.new_currency
            })
          }
        }

We extract the converted property from our state and use it to keep track of the payload to send back.

For our defaultCurrency we pass the oldAmount, oldCurrency and newCurrency states into the component, then we extract the converted state to send this information to our fetchCurrency function while the states we destructure into the component is used to dynamically show what value to display using the converted property as a boolean to render the value we want.

Lastly, we use debounce function from lodash-debounce package. and defer our api calls by 270ms.

// imported useSelector, useDispatch and others

const DefaultCurrency = ({oldAmount, oldCurrency, newCurrency}) => {
    const converted = useSelector((state) => state.payloadReducer.converted)
    const dispatch = useDispatch()
    const callForData = debounce((value) => {
      dispatch(fetchCurrency(value, oldCurrency, newCurrency, converted))
    }, 270) // we wait 270ms before sending the request
    const handleInput = (e) => {
        const name = e.target.name
        const value = Number(e.target.value)
        dispatch({type: "LOADING"}) // loading state starts
        dispatch({type: "UNCONVERT"}) // convert property set to false
        dispatch({type: "CHANGE", [name]: value}) // controls input
        return callForData(value)
    }

    return (
        <CurrencyInput type="number" name="old_amount" id="defaultTypeAmount" 
                value={converted ? "" : oldAmount || ""} // if convert is true, make the value empty, else replace with old value, if old value is null leave it  empty
                placeholder="0"
                onChange={handleInput} 
                min={0}
                max={999999}
                step={0.5}
                />
    )
}

export default DefaultCurrency

Similar is done with the ConvertedCurrency component.

// imported useSelector, useDispatch and others

const ConvertedCurrency = ({newAmount, newCurrency, oldCurrency}) => {

    const converted = useSelector((state) => state.payloadReducer.converted)

    const dispatch = useDispatch()

    const callForData = debounce((value) => {
        let olderCurrency = newCurrency
        let newerCurrency = oldCurrency
        dispatch({type: "LOADING"})
        dispatch(fetchCurrency(value, olderCurrency, newerCurrency, converted))
      }, 270)

    const handleInput = (e) => {
        const name = e.target.name
        const value = Number(e.target.value)
        dispatch({type: "CONVERT"})
        dispatch({type: "CHANGE", [name]: value})
        return callForData(value)
    }

    return (
        <CurrencyInput type="number" name="new_amount" id="convertedTypeAmount"
        value={!converted ? "" : newAmount || ""}
        placeholder="0"
        onChange={handleInput}
        min={0}
        max={999999}
        step={0.5}
        />
    )
}

export default ConvertedCurrency

The last thing that we do is create the logic for the labels as they would be conditionally displaying the value we want based on what input was activated.

The DefaultLabel component accepts 2 parameters, the first is the oldAmount property prom our currencyReducer, the second is the oldCurrency reducer, we are also extracting our payloadReducer's state.

const DefaultLabel = ({oldAmount = 0, oldCurrency}) => {

    const converted = useSelector((state) => state.payloadReducer.converted)
    const payloadAmount = useSelector((state) => state.payloadReducer.payload_amount)
    const payloadCurrency = useSelector((state) => state.payloadReducer.payload_currency)
    const loading = useSelector((state) => state.payloadReducer.loading)

    return (
        <Label htmlFor="defaultTypeAmount">
                    <ConvertTitle>Converted <br/>{!converted ? "from" : "to"}</ConvertTitle>
                    {
                        !converted && !loading ? 
                        (   
                                oldAmount.toLocaleString("en-US", {
                                style: "currency",
                                currency: `${oldCurrency}`,
                                minimumFractionalDigits: 2
                        }))
                       :
                        !converted && loading ? (
                                oldAmount.toLocaleString("en-US", {
                                style: "currency",
                                currency: `${oldCurrency}`,
                                minimumFractionalDigits: 2
                        })
                        ) :
                        (   
                                payloadAmount.toLocaleString("en-US", {
                                style: "currency",
                                currency: `${payloadCurrency || oldCurrency}`,
                                minimumFractionalDigits: 2
                        })
                        ) 
                    }
                </Label>
    )
}

export default DefaultLabel

We use the converted property to determine what the label should display, if the value and loading are false it displays the oldAmount, else if the value of the converted property is false and loading is true we still show the oldAmount, else we show the payloadAmount.

We do a similar thing for the ConvertedLabel component.

Wonderful, we're done implementing the main functionality of our Currency Converter application, more will still be added, like a cryptocurrency swap function and routing but for now the logic for the app is complete.