Get currency rates based on currency selection - reactjs

When we enter some value in text box and currency in the fromCurrency dropdown field and select appropriate currency in the toCurrency dropdown field, how do we display rates in the toCurrency based on that selection ?
https://codesandbox.io/s/rough-http-jc35u?file=/src/App.js
import React, { useState, useEffect } from "react";
import "./styles.css";
const axios = require("axios");
function App() {
const [sourceCurrency, setSourceCurrency] = useState("");
const [targetCurrency, setTargetCurrency] = useState("");
const [ratesList, setRatesList] = useState([]);
const [selectFromCurrency, setFromSourceCurrency] = useState("");
const [selectToCurrency, setSelectToCurrency] = useState("");
const getSourceCurrency = (source) => {
setSourceCurrency(source);
};
const getTargetCurrency = (target) => {
setTargetCurrency(target);
};
useEffect(() => {
const fetchData = async () => {
try {
const data = await axios.get("https://api.exchangeratesapi.io/latest");
setRatesList(data);
console.log(data);
} catch (e) {
console.log(e);
}
};
fetchData();
}, []);
const selectSourceCurrency = (sourceCurr) => {
setFromSourceCurrency(sourceCurr);
};
const selectTargetCurrency = (targetCurr) => {
setSelectToCurrency(targetCurr);
};
const convertRate = () => {
const rateCalc = sourceCurrency * targetCurrency;
console.log("print rate: " + rateCalc);
// how can we the rates list here and based on the selection ?
};
return (
<div className="App">
<div className="globalCurrencyConverter">
<h2>Currency Converter</h2>
<div className="container box">
<label>
<input
name="sourceCurrency"
type="text"
placeholder="fromCurrency"
onChange={(event) => getSourceCurrency(event.target.value)}
/>
<select
className="fromCurrency"
defaultValue={"DEFAULT"}
onChange={(event) => selectSourceCurrency(event.target.value)}
>
<option>USD</option>
<option value="DEFAULT">AUD</option>
<option>NZD</option>
<option>INR</option>
<option>UAE Dirham</option>
</select>
</label>
<label>
<input
name="targetCurrency"
type="text"
placeholder="toCurrency"
onChange={(event) => getTargetCurrency(event.target.value)}
/>
<select
className="toCurrency"
onChange={(event) => selectTargetCurrency(event.target.value)}
>
<option>USD</option>
<option>AUD</option>
<option>NZD</option>
<option>INR</option>
<option>UAE Dirham</option>
</select>
</label>
<div className="recordBtn">
<button name="convert" onClick={(event) => convertRate()}>
Convert
</button>
</div>
</div>
</div>
</div>
);
}
export default App;

I will assume that you can handle the population of those select fields with currencies yourself and instead will show you how to solve the actual conversion problem. So we shall leave those select options hardcoded as they are in your code. e.g. (USD, NZD, AUD etc.)
So we won't actually even need that useEffect for this test since we simply hardcode the currencies. Personally, I like to solve my React problems with as little re-renders as possible. So the way I would approach this specific problem is by creating references to all 4 of your fields. It will allow us to access their values any time. Check out useRef().
Then when someone enters all the info and clicks that "Convert" button, I would call your API and pass it the selected currency as base currency. like so
https://api.exchangeratesapi.io/latest?base=USD
Once axios fetches the data on it, it is just a matter of some basic match and assignment of the proper value to the "To Currency" field. So here is a working example along with a Sandbox:
import React, { useState, useEffect, useRef } from "react";
import "./styles.css";
const axios = require("axios");
function App() {
const from_select = useRef(),
to_select = useRef(),
from_input = useRef(),
to_input = useRef();
useEffect(() => {
const fetchData = async () => {
try {
const data = await axios.get("https://api.exchangeratesapi.io/latest");
//setRatesList(data);
console.log(data);
} catch (e) {
console.log(e);
}
};
fetchData();
}, []);
const convertRate = () => {
const from_cur = from_select.current.value;
const to_cur = to_select.current.value;
const from_amount = from_input.current.value;
console.log(from_cur);
axios
.get("https://api.exchangeratesapi.io/latest?base=" + from_cur)
.then((result) => {
const rate = result.data.rates[to_cur];
const converted_amount = rate * from_amount;
to_input.current.value = converted_amount;
});
};
return (
<div className="App">
<div className="globalCurrencyConverter">
<h2>Currency Converter</h2>
<div className="container box">
<label>
<input
ref={from_input}
name="sourceCurrency"
type="text"
placeholder="fromCurrency"
/>
<select
ref={from_select}
className="fromCurrency"
defaultValue={"USD"}
>
<option value="USD">USD</option>
<option value="AUD">AUD</option>
<option value="NZD">NZD</option>
</select>
</label>
{" -> "}
<label>
<input
ref={to_input}
name="targetCurrency"
type="text"
placeholder="toCurrency"
/>
<select ref={to_select} className="toCurrency" defaultValue="AUD">
<option value="USD">USD</option>
<option value="AUD">AUD</option>
<option value="NZD">NZD</option>
<option value="RUB">RUB</option>
<option value="EUR">EUR</option>
</select>
</label>
<div className="recordBtn">
<button name="convert" onClick={convertRate}>
Convert
</button>
</div>
</div>
</div>
</div>
);
}
export default App;

your ratesList would be an object extracted from data.data.rates with country keys and rate values set at initial useEffect as:
useEffect(() => {
const fetchData = async () => {
try {
const data = await axios.get("https://api.exchangeratesapi.io/latest");
setRatesList(data.data.rates);
} catch (e) {
console.log(e);
}
};
fetchData();
}, []);
your convertRate validates first if sourceCurrency is a number and if there is a ratesList. To calculate the conversion you need to multiply the amount value by the ratio (toCurrency/FromCurrency):
const convertRate = () => {
if (isNaN(sourceCurrency) || !ratesList) return;
setTargetCurrency(
(ratesList[selectToCurrency] / ratesList[selectFromCurrency]) *
sourceCurrency
);
};
set initial values for currencies:
const [selectFromCurrency, setFromSourceCurrency] = useState("USD");
const [selectToCurrency, setSelectToCurrency] = useState("NZD");
and remove default values for your select and input values. Instead pass the state value to have a controlled input like:
<select
className="fromCurrency"
value={selectFromCurrency}
onChange={(event) => selectSourceCurrency(event.target.value)}
>
<option>USD</option>
<option>AUD</option>
<option>NZD</option>
<option>INR</option>
<option>PLN</option>
</select>
for your toCurrency input make it a disabled field, since you don't user to type values on it:
<input
name="targetCurrency"
value={targetCurrency}
disabled
type="text"
placeholder="toCurrency"
/>
working demo:
note: UAE Dirham doesn't match at API response so changed for PLN

Related

Why does console log inside setState function when called from child component gets access to updated state value?

Below are the two mentioned cases when console logging a state results in different outputs.
CASE1: Console logging currentTitle state logs the previous state even after updating state in titleChangeHandler function.
See Case1 Console Log
import "./ExpenseForm.css";
const ExpenseForm = (props) => {
const [currentTitle, setCurrentTitle] = useState("");
const [currentAmount, setCurrentAmount] = useState("");
const [currentDate, setCurrentDate] = useState("");
const titleChangeHandler = (event) => {
setCurrentTitle(event.target.value);
console.log(currentTitle);
};
const amountChangeHandler = (event) => {
setCurrentAmount(event.target.value);
};
const dateChangeHandler = (event) => {
setCurrentDate(event.target.value);
};
const submitHandler = (event) => {
event.preventDefault();
const expenseData = {
title: currentTitle,
amount: currentAmount,
date: currentDate,
};
props.onSaveExpenseData(expenseData);
setCurrentAmount("");
setCurrentTitle("");
setCurrentDate("");
};
return (
<form onSubmit={submitHandler}>
<div className="new-expense__controls">
<div className="new-expense__control">
<label>Title</label>
<input
type="text"
value={currentTitle}
onChange={titleChangeHandler}
/>
</div>
<div className="new-expense__control">
<label>Amount</label>
<input
type="number"
min="0.1"
step="0.1"
value={currentAmount}
onChange={amountChangeHandler}
/>
</div>
<div className="new-expense__control">
<label>Date</label>
<input
type="date"
min="2019-01-01"
max="2022-12-31"
value={currentDate}
onChange={dateChangeHandler}
/>
</div>
</div>
<div className="new-expense__actions">
<button type="submit">Add expense</button>
</div>
</form>
);
};
export default ExpenseForm;
CASE 2: When we pass a function(onSelectYear) to a child component using props, and call setState function(setYear) inside the passed function, when invoked from child component(ExpensesFilter), then console logging the state shows the latest value after updating.
See Case2 Console Log
// Expenses.js
import "./Expenses.css";
import ExpenseItem from "./ExpenseItem";
import React, { useState } from "react";
import ExpensesFilter from "./ExpenseFilter";
import Card from "../UI/Card";
const Expenses = (props) => {
const [year, setYear] = useState("2020");
const onSelectYear = (year) => {
setYear(year);
console.log(year);
};
return (
<div>
<Card className="expenses">
<ExpensesFilter
selectedYear={year}
onSelectYear={onSelectYear}
></ExpensesFilter>
<ExpenseItem
title={props.expenses[0].title}
amount={props.expenses[0].amount}
date={props.expenses[0].date}
></ExpenseItem>
<ExpenseItem
title={props.expenses[1].title}
amount={props.expenses[1].amount}
date={props.expenses[1].date}
></ExpenseItem>
<ExpenseItem
title={props.expenses[2].title}
amount={props.expenses[2].amount}
date={props.expenses[2].date}
></ExpenseItem>
<ExpenseItem
title={props.expenses[3].title}
amount={props.expenses[3].amount}
date={props.expenses[3].date}
></ExpenseItem>
</Card>
</div>
);
};
export default Expenses;
// ExpenseFilter.js
import React from "react";
import "./ExpenseFilter.css";
const ExpensesFilter = (props) => {
const onSelectChange = (event) => {
props.onSelectYear(event.target.value);
};
return (
<div className="expenses-filter">
<div className="expenses-filter__control">
<label>Filter by year</label>
<select value={props.selectedYear} onChange={onSelectChange}>
<option value="2022">2022</option>
<option value="2021">2021</option>
<option value="2020">2020</option>
<option value="2019">2019</option>
</select>
</div>
</div>
);
};
export default ExpensesFilter;
I understand that react schedules the state changes and this operation is async in nature. Thats why an immediate console log doesn't reflect updated value. But in the second case, we are essentially doing the same. Can anyone please explain, what makes the second case different from first?
If we look at this part of the code:
const [year, setYear] = useState("2020");
const onSelectYear = (year) => {
setYear(year);
console.log(year);
};
The year in console.log(year) is the value that is being passed as an argument to the function onSelectYear
const [year /* this is out of scope for onSelectYear */ , setYear] = useState("2020");
const onSelectYear = (year /* this value */) => {
setYear(year);
console.log(year); // is being logged here
};
On changing the function definition like so:
const [year /* this is now in scope for onSelectYear */ , setYear] = useState("2020");
const onSelectYear = (newYear /* changed the name */) => {
setYear(newYear);
console.log(year); // the year as defined by useState is now being logged here
};
The same observation will be seen as Case 1.

Id invalid in react select, axios post request to API

I'm trying to add a Leader to my DB via a post.
I want to select a branch so the leader is linked to that branch.
but When I select the branch from the select box the Id does not get filled and I get the following error:
.branchId: [,…]
0: "The JSON value could not be converted to System.Guid. Path: $.branchId | LineNumber: 0 | BytePositionInLine: 30."
been stuck for a day now, help is muich appreciated
import React, {useState, useEffect} from 'react'
import axios from 'axios'
const LeadersPost = () => {
const [totem, setTotem] = useState('');
const [branchId, setBranchId] = useState('')
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async() => {
try{
const {data: response} = await axios.get('https://localhost:7070/api/Branches');
setData(response);
} catch (error) {
console.error(error.message);
}
}
fetchData();
}, []);
const onSubmit = async (e) => {
e.preventDefault();
const data = {
totem : totem,
branchId : branchId
}
try{
await axios.post('https://localhost:7070/api/Leaders', data)
.then(res => {
setData(res.data);
setTotem('');
setBranchId('');
})
} catch (error){
console.error(error.message);
}
}
return(
<div className='container mt-2'>
<h2>Leaders Post Request</h2>
<form onSubmit={onSubmit}>
<div className='mb-2 mt-3'>
<input
type={'text'}
placeholder='Leader Totem'
className='form-control'
value={totem}
onChange={e => {
setTotem(e.target.value)
}} />
<select className="form-control" aria-label="Default select example">
<option>Choose a branch</option>
{
data.map(branch =>
<option
onChange={ e => {
setBranchId(e.target.value)
}}>
{branch.id}
</option>)
}
</select>
<button type='submit' className='btn btn-primary'>Create</button>
</div>
</form>
</div>
)
}
export default LeadersPost
I don't believe the option fires an onchange event, I could be wrong but if my memory serves me correctly it doesn't. Also just checked MDN web docs and I may be correct in that.
However, I also believe the value of the option not being set would also cause this to happen. You're just setting a textConext on the event, so you could make it work potentially by that too but you may want to take a look at this answer here for select boxes in react.

(Reactjs) "delay" when fetching data using useEffect and useState

The below is my code.
What i am doing is a currency converter app by fetching external API. It is guaranteed process.env.REACT_APP_DATA_SOURCE is always correct.
The problem here is: If I change the currency, either inputCurrency or outputCurrency, I need to click "Submit" button twice instead of once to display a correct answer. Why is it the case?
I can't come up with any better wording so I used "delay" in the heading.
Thank you!
import './App.css';
import { useState, useEffect } from 'react';
import { Select, Button, ButtonGroup } from '#chakra-ui/react';
function App() {
const [exchangeRate, setExchangeRate] = useState(7.8491);
const [inputCurrency, setInputCurrency] = useState('USD');
const [inputAmount, setInputAmount] = useState(1);
const [outputCurrency, setOutputCurrency] = useState('HKD');
const [outputAmount, setOutputAmount] = useState(7.8491);
useEffect(() => {
const fetchData = async (url) => {
await fetch(url)
.then((response) => response.json())
.then((data) => setExchangeRate(data.conversion_rates[outputCurrency]));
};
try {
let url = `${process.env.REACT_APP_DATA_SOURCE}${inputCurrency}`;
fetchData(url);
} catch (err) {
console.log(err);
}
}, [inputCurrency, outputCurrency]);
const handleSubmit = async (e) => {
e.preventDefault();
setInputCurrency(e.target.inputCurrency.value);
setInputAmount(e.target.inputAmount.value);
setOutputCurrency(e.target.outputCurrency.value);
let result = inputAmount * exchangeRate;
setOutputAmount(result);
};
return (
<div className="main-body">
<form onSubmit={handleSubmit}>
<h3>Set input amount:</h3>
<input className="inputAmount" name="inputAmount" type="number" />
<h3>Set input currency:</h3>
<Select name="inputCurrency">
<option value="USD">USD</option>
<option value="HKD">HKD</option>
<option value="CNY">CNY</option>
<option value="GBP">GBP</option>
<option value="CAD">CAD</option>
</Select>
<h3>Set output currency:</h3>
<Select name="outputCurrency">
<option value="USD">USD</option>
<option value="HKD">HKD</option>
<option value="CNY">CNY</option>
<option value="GBP">GBP</option>
<option value="CAD">CAD</option>
</Select>
<Button colorScheme="blue" type="submit">
Submit
</Button>
</form>
<h3>
{inputAmount} {inputCurrency} is equal to:
</h3>
<h3>
{outputAmount} {outputCurrency}
</h3>
</div>
);
}
export default App;
You are calculating the output amount at the wrong place. First, you update the inputCurrency and outputCurrency in handleSubmit. This means the useEffect will be 'triggered' and the new exchange rate will be fetched. But that's not gonna happen immediately. Definitely not before result is calculated.
const handleSubmit = async (e) => {
e.preventDefault();
setInputCurrency(e.target.inputCurrency.value);
setInputAmount(e.target.inputAmount.value);
setOutputCurrency(e.target.outputCurrency.value);
//This will ALWAYS use the old previous exchange rate.
let result = inputAmount * exchangeRate;
setOutputAmount(result);
};
What you want is to recalculate outputAmount whenever the exchangeRate or inputAmount changes. You can use another useEffect like:
useEffect(() => {
//Take these out from handleSubmit.
let result = inputAmount * exchangeRate;
setOutputAmount(result);
}, [inputAmount, exchangeRate])

next.js and useSWR makes an error "Too many re-renders" and I'm not sure why?

I'm new to React and even more new to Next.js
I've got an input where the user search a city by name in a list of all the cities available.
I've read that useSWR could be interesting to use (before that I made the request with axios inside a useEffect).
Once I got the array of cities I do a map to return all the matches with the request (and then I use it to do an autocomplete).
But I get an error like this :
"Error: Too many re-renders. React limits the number of renders to prevent an infinite loop."
If I just fetch the data, it works but if I do the map on the array I get an infinite loop and I don't know why.
my code :
import React, { useState, useEffect } from "react";
import styles from "./searchBar.module.css";
import { useRouter } from "next/router";
import axios from "axios";
import useSWR from "swr";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import { faSearch, faAngleDown } from "#fortawesome/free-solid-svg-icons";
import installationType from "../public/installation_type_ids.json";
const SearchBar = ({
setSearchSport,
setSearchCity,
searchCity,
searchSport,
setSearchType,
searchType,
setPage,
}) => {
const router = useRouter();
// States for search bar request
const [city, setCity] = useState(searchCity);
const [cityData, setCityData] = useState([]);
const [sport, setSport] = useState(searchSport);
const [title, setTitle] = useState("");
const [type, setType] = useState(searchType);
const [autoComplete, setAutoComplete] = useState([]);
const [displayAutoComplete, setDisplayAutoComplete] = useState(false);
// handle submit button
const handleSubmit = (e) => {
e.preventDefault();
setSearchCity(city);
setSearchSport(sport);
type ? setSearchType(type) : setSearchType("ALL");
setPage(0);
if (router.pathname !== "/points-de-rencontre-sportive")
router.push("/points-de-rencontre-sportive");
};
const url = "https://bouge-api.herokuapp.com/v1.0/city/ids";
const fetcher = (...args) => fetch(...args).then((res) => res.json());
const { data: result, error } = useSWR(url, fetcher);
if (error) return <h1>Oups ! Une erreur est survenue...</h1>;
if (!result) return <h1>Chargement en cours...</h1>;
const handleTest = (e) => {
setCity(e.target.value);
e.target.value === 0
? setDisplayAutoComplete(false)
: setDisplayAutoComplete(true);
if (result && result.data) {
const dataMapped = result.data.map((city) => {
return { city: city.name, type: "city" };
});
let tab = [];
dataMapped.map((item, i) => {
item.name
if (item.name.toLowerCase().includes(city)) {
tab.push(item);
}
return setAutoComplete(tab);
});
}
};
// autocomplete rendering
const renderAutoComplete = autoComplete.map((elem, index) => {
console.log(elem);
if (index <= 9) {
return (
<div
className={styles.autocompleteDiv}
key={index}
onClick={() => {
if (elem.type === "city") {
setCity(elem.city);
}
if (elem.type === "sport") {
setSport(elem.sport);
}
setDisplayAutoComplete(false);
}}
>
<p>{elem.type === "city" ? elem.city : elem.sport}</p>
</div>
);
} else {
return null;
}
});
return (
<div className={styles.searchBar}>
<form className={styles.form} onSubmit={handleSubmit}>
<div>
<label htmlFor="city">Ville</label>
<input
type="text"
id="city"
placeholder="Où veux-tu jouer?"
value={city}
onChange={handleTest}
autoComplete="off" // disable chrome auto complete
/>
</div>
<div>
<label htmlFor="sport">Sport</label>
<input
type="text"
id="sport"
placeholder="Spécifie le sport"
value={sport}
onChange={(e) => {
setSport(e.target.value);
}}
autoComplete="off" // disable chrome auto complete
/>
</div>
<div>
<label htmlFor="title">Nom</label>
<input
type="text"
id="title"
placeholder="Recherche par nom"
value={title}
onChange={(e) => {
setTitle(e.target.value);
let tab = [];
installationType.map((item, i) => {
if (item.installation_type.includes(title)) {
tab.push(item);
}
return setAutoComplete(tab);
});
console.log(tab);
}}
autoComplete="off" // disable chrome auto complete
/>
</div>
<div>
<label htmlFor="type">Type</label>
<select
type="text"
id="type"
placeholder="Type de structure"
value={type}
>
<option value="ALL" defaultValue>
Type de structure
</option>
<option value="AS">Association</option>
<option value="PRIV">Privé</option>
<option value="PUB">Public</option>
<option value="EVENT">Activité</option>
</select>
<i>
<FontAwesomeIcon
icon={faAngleDown}
className={styles.selectIcon}
></FontAwesomeIcon>
</i>
</div>
<button>
<i>
<FontAwesomeIcon
icon={faSearch}
className="fa-lg"
></FontAwesomeIcon>
</i>
Rechercher
</button>
</form>
{displayAutoComplete ? (
<div className={styles.searchResults}>{renderAutoComplete}</div>
) : null}
</div>
);
};
export default SearchBar;
After you fetched data, you call setCityData method to update city data, this cause component re-render and run code in your SearchBar component again, so it call setCityData again and then continue re-render => infinite re-render.
I think you should put it into a useEffect:
useEffect(() => {
if (result && result.data) {
const dataMapped = result.data.map((city) => {
return { city: city.name, type: "city" };
});
setCityData(dataMapped)
}
}, [result])
so it will update city data only when result has data

Set title onSubmit based on current SELECT OPTION VALUE - react

I found this tutorial on how to create a react quizz app on youtube link to tutorial
I am trying to set the title based on the current Select Option Value when submitting the form.
Currently I managed to change the title only when a different option is selected.
import React, { useState, useEffect, useRef } from "react";
import "./App.css";
import axios from "axios";
import FlashcardList from "./components/FlashcardList";
function App() {
const [flashcards, setFlashcards] = useState([]);
const [categories, setCategories] = useState([]);
const [title, setTitle] = useState("General Knowledge");
const categoryEl = useRef();
const amountEl = useRef();
useEffect(() => {
axios.get("https://opentdb.com/api_category.php").then((res) => {
setCategories(res.data.trivia_categories);
});
}, []);
function decodeString(str) {
const textArea = document.createElement("textarea");
textArea.innerHTML = str;
return textArea.value;
}
function handleSubmit(e) {
e.preventDefault();
axios
.get("https://opentdb.com/api.php", {
params: {
amount: amountEl.current.value,
category: categoryEl.current.value,
},
})
.then((res) => {
setFlashcards(
res.data.results.map((questionItem, index) => {
const answer = decodeString(questionItem.correct_answer);
const options = [...questionItem.incorrect_answers, answer];
return {
id: `${index} - ${Date.now()}`,
question: decodeString(questionItem.question),
answer: answer,
options: options.sort(() => Math.random() - 0.5),
};
})
);
});
}
function getTitle(e) {
setTitle(e.target.options[e.target.selectedIndex].text);
}
return (
<>
<form className="header" onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="category">Category</label>
<select id="category" ref={categoryEl} onChange={getTitle}>
{categories.map((category) => {
return (
<option value={category.id} key={category.id}>
{category.name}
</option>
);
})}
</select>
</div>
<div className="form-group">
<label htmlFor="amount">Number Of Questions</label>
<input
type="number"
id="amount"
min="1"
step="1"
defaultValue={10}
ref={amountEl}
/>
</div>
<div className="form-group">
<button className="btn">Generate</button>
</div>
</form>
<div className="container">
<h1 className="title">{title}</h1>
<FlashcardList flashcards={flashcards} />
</div>
</>
);
}
export default App;
Code
Live demo
You can set the category as soon the categories are fetched. Can just use the zeroth element to set the title.
useEffect(() => { axios.get("https://opentdb.com/api_category.php").then((res) => {
setCategories(res.data.trivia_categories);
setTitle(res.data.trivia_categories[0]);
});
}, []);

Resources