onChange not updating React - reactjs

I've been working on this project for the last couple of hours and I was pretty sure that this final hour would be my last. No errors are appearing. My thinking is that when I pick a hero from the drop down, the page will update depending on my choice. I may have something that isn't firing that I'm not picking up on.
import React, {useEffect, useState} from 'react'
import axios from 'axios'
require("regenerator-runtime/runtime");
const App = () => {
const [hero, selectedHero] = useState(
'Select a Hero'
);
const handleChange = event => selectedHero(event.target.value);
return(
<HeroSelect heroSelect={hero} onChangeHeadline={handleChange} />
);
};
const HeroSelect = ({heroSelect, onChangeHeadline}) => {
const [data, setData] = useState({heroes: []});
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://api.opendota.com/api/heroStats',
);
setData({...data, heroes: result.data});
};
fetchData();
}, []);
return (
<div>
<h1>{heroSelect}</h1>
<select>
{data.heroes.map(item => (
<option key={item.id} value={heroSelect} onChange={onChangeHeadline} >
{item.localized_name}
</option>
))}
</select>
</div>
)
};
export default App

Define your onChange={onChangeHeadline} on Select tag not on option tag
<select onChange={onChangeHeadline}>
{data.heroes.map(item => (
<option key={item.id} value={item.localized_name}>
{item.localized_name}
</option>
))}
</select>

You should be firing your onChange event on the select tag itself.
<select onChange={onChangeHeadline} >
.....
.....
</select>

I reckon you didn't declare an onChange on the select.

Using This method:
<select id="lang" onChange={this.change} value={this.state.value}>
<option value="select">Select</option>
<option value="Java">Java</option>
<option value="C++">C++</option>
</select>

Related

Cannot display 1 image from a Array from a API

I can manage to display all and have them in roughly the right place with CSS, however in the case where there is more than 1 image, I cannot display just that first image
I eventually want to be able to click the image and display a Modal showing a gallery of these pics, but stuck on this part:
import React, { useState, useEffect } from "react";
import "./Hotel.css";
const URL = "https://obmng.dbm.guestline.net/api/hotels?collection-id=OBMNG";
const Hotel = () => {
const [hotel, setHotel] = useState([]);
useEffect(() => {
hotels();
}, []);
const hotels = async () => {
const response = await fetch(URL);
setHotel(await response.json());
};
// filter hotels button displayed by star rating
const filterHotels = (e) => {
const starRating = e.target.value;
const filteredHotels = hotel.filter(
(hotel) => hotel.starRating === starRating
);
setHotel(filteredHotels);
};
// store filteredHotels in state
const [filteredHotels, setFilteredHotels] = useState([]);
useEffect(() => {
setFilteredHotels(hotel);
}, [hotel]);
return (
<div>
<div className="selection-filter">
{/* drop down for useState */}
<label for="filter">Filter by star rating: </label>
<select onChange={filterHotels}>
<option value="0">All</option>
<option value="1">1 Star</option>
<option value="2">2 Star</option>
<option value="3">3 Star</option>
<option value="4">4 Star</option>
<option value="5">5 Star</option>
</select>
</div>
{hotel.map((data) => {
return (
<div>
<div className="list-group-item hotel-area" key={data.id}>
<div className="hotel-name">{data.name}</div>
{data.images.map((image) => (
<img
className="hotel-image"
src={data.images[0].url}
alt={image.alt}
/>
))}
<div className="hotel-address">{data.address1}</div>
<div className="hotel-address">{data.address2}</div>
<div className="star-rating fas fa-star">{data.starRating}</div>
<hr />
</div>
</div>
);
})}
</div>
);
};
export default Hotel;
It displays 3 images, because the first hotel object in the response object has 3 elements in the images array. If you want to show only one (first) image, don't use .map function, just render the first image, see this example.

React Testing Library works on HTML elements but not with React components

Basically, I'm trying to test a component that have a select.
When trying to test the component, the test fails by returning the default value instead of the changed value.
But when I take the HTML of the rendered component (from screen.debug()) it works.
The component:
export function SelectFile({
fileList,
handleChange,
selected,
}) {
return (
<select
className="bg-slate-600 rounded w-auto"
onChange={onChange}
value={selected}
>
<option value="">Select an option</option>
<TodayOptions />
<AllOptions />
</select>
);
function AllOptions() {
return (
<>
{Object.entries(groups).map(([key, value]) => {
return (
<optgroup key={key} label={key.toLocaleUpperCase()}>
{[...value].sort(sortByDateFromLogs).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</optgroup>
);
})}
</>
);
}
function TodayOptions() {
const todayFiles = Object.values(groups)
.map((group) => {
const today = new Date().toLocaleDateString().replace(/\//g, '-');
return group.filter((file) => file.includes(today));
})
.flat();
if (todayFiles.length === 0) {
return null;
}
return (
<optgroup label="Today">
{todayFiles.map((item) => (
<option key={item}>{item}</option>
))}
</optgroup>
);
}
}
The original test:
it('should change option', () => {
render(
<SelectFile
fileList={fileList}
handleChange={handleChange}
selected=""
/>,
);
const selectElement = screen.getByDisplayValue('Select an option');
const allOptions = screen.getAllByRole('option');
const optionSelected = fileList.adonis[1];
expect(selectElement).toHaveValue('');
act(() => {
userEvent.selectOptions(selectElement, optionSelected);
});
expect(handleChange).toHaveBeenCalledTimes(1);
expect(selectElement).toHaveValue(optionSelected); // returns "" (default value)
expect((allOptions[0] as HTMLOptionElement).selected).toBe(false);
expect((allOptions[1] as HTMLOptionElement).selected).toBe(true);
expect((allOptions[2] as HTMLOptionElement).selected).toBe(false);
expect((allOptions[3] as HTMLOptionElement).selected).toBe(false);
expect((allOptions[4] as HTMLOptionElement).selected).toBe(false);
});
And the modified test with the rendered html:
it('should change option', () => {
render(
<div>
<div className="flex mr-10">
<h3 className="text-lg font-bold mr-4">Select a file</h3>
<select className="bg-slate-600 rounded w-auto">
<option value="">Select an option</option>
<optgroup label="ADONIS">
<option value="adonis-03-02-2022.json">
adonis-03-02-2022.json
</option>
<option value="adonis-02-02-2022.json">
adonis-02-02-2022.json
</option>
</optgroup>
<optgroup label="ERRORS">
<option value="http_errors-03-03-2022.log">
http_errors-03-03-2022.log
</option>
<option value="http_errors-04-02-2022.log">
http_errors-04-02-2022.log
</option>
</optgroup>
</select>
</div>
</div>,
);
const selectElement = screen.getByDisplayValue('Select an option');
const allOptions = screen.getAllByRole('option');
const optionSelected = fileList.adonis[1];
expect(selectElement).toHaveValue('');
act(() => {
userEvent.selectOptions(selectElement, optionSelected);
});
expect(selectElement).toHaveValue(optionSelected); // this returns the optionSelected value
expect((allOptions[0] as HTMLOptionElement).selected).toBe(false);
expect((allOptions[1] as HTMLOptionElement).selected).toBe(true);
expect((allOptions[2] as HTMLOptionElement).selected).toBe(false);
expect((allOptions[3] as HTMLOptionElement).selected).toBe(false);
expect((allOptions[4] as HTMLOptionElement).selected).toBe(false);
});
Considering it works with the modified test, I can't make it why it doesn't on the original.
I've considered it was due to the optgroup, but it doesn't seems the case, so now I'm at a loss as to why.
Edit: the final version of the test:
it('should change option', () => {
const mockHandleChange = handleChange.mockImplementation(
(cb) => (e) => cb(e.target.value),
);
render(
<SelectWrapper fileList={fileList} handleChange={mockHandleChange} />,
);
const selectElement = screen.getByDisplayValue('Select an option');
const optionSelected = fileList.adonis[1];
expect(selectElement).toHaveValue('');
act(() => {
userEvent.selectOptions(selectElement, optionSelected);
});
expect(handleChange).toHaveBeenCalledTimes(2); // 1 for cb wrapper, 1 for select
expect(selectElement).toHaveValue(optionSelected);
});
});
const SelectWrapper = ({ handleChange, fileList }) => {
const [selected, setSelected] = useState('');
const mockHandleChange = handleChange(setSelected);
return (
<SelectFile
fileList={fileList}
handleChange={mockHandleChange}
selected={selected}
/>
);
};
I've created a wrapper to make it like you would use in another component, wrapped the mock function and now it changes the value and you have access to the mock.
Since in your test you are rendering only the Select (which is a controlled component : it receives from its parent the current value and a onChange callback), with a fixed selected props, you cannot expect the selected option to change when you trigger a change event on the select. You can only expect that the onChange callback has been called (like you do).
For this kind of component, you need to test that the selected props is respected (the selected option is the good one), and that the provided callback is called when the user chooses a new option (you ahve done this part).
You need to add a test with an existing option as selected props (not empty string), then check that the selected option is the right one. I suggest you use https://github.com/testing-library/jest-dom#tohavevalue from https://github.com/testing-library/jest-dom.

Using useRef in inside a function handler with hooks

I'm trying to submit a form that sends a http request onSubmit, but I'm getting undefined when setting the state on the values. I'm not sure why the values are not being passed upon click and being set with the set...() function.
Below is the code of that component. Upon first submit action I get an error because the "surveyUserAnswers" are undefined, but in the next submissions it works. Not sure why? Could someone advise.
I'm very new to typescript and react hooks, so excuse my code! thanks
import React, { useRef, useState } from "react";
import Loader from "../UI/loader/loader";
import axios from "axios";
import "./surveybox.css";
interface surveryAnswer {
id: number;
answers: string[];
}
const SurveyBox: React.FC = () => {
const [surveyUserAnswers, setSurveyUserAnswers] = useState<surveryAnswer>();
const [loading, setLoading] = useState(false);
const programmingQRef = useRef<HTMLSelectElement>(null);
const skillsQRef = useRef<HTMLSelectElement>(null);
const stateManagementQRef = useRef<HTMLSelectElement>(null);
const programmerTypeQRef = useRef<HTMLSelectElement>(null);
const onSubmitSurvey = (e: React.FormEvent): void => {
e.preventDefault();
setLoading((prevLoading) => !prevLoading);
setSurveyUserAnswers({
id: Math.random(),
answers: [
programmerTypeQRef.current!.value,
skillsQRef.current!.value,
stateManagementQRef.current!.value,
programmerTypeQRef.current!.value,
],
});
axios
.post(`${DB_URL}/users-answers.json`, surveyUserAnswers)
.then((res) => {
setLoading((prevLoading) => !prevLoading);
})
.catch((error) => {
console.log(error);
setLoading((prevLoading) => !prevLoading);
});
};
return (
<div className="surveybox-container">
{loading ? (
<div className={"loader-holder"}>
<Loader />
</div>
) : (
<React.Fragment>
<h2>Quick survey!</h2>
<form action="submit" onSubmit={onSubmitSurvey}>
<label>favorite programming framework?</label>
<select ref={programmingQRef} name="programming">
<option value="React">React</option>
<option value="Vue">Vue</option>
<option value="Angular">Angular</option>
<option value="None of the above">None of the above</option>
</select>
<br></br>
<label>what a junior developer should have?</label>
<select ref={skillsQRef} name="skills">
<option value="Eagerness to lear">Eagerness to learn</option>
<option value="CS Degree">CS Degree</option>
<option value="Commercial experience">
Commercial experience
</option>
<option value="Portfolio">Portfolio</option>
</select>
<br></br>
<label>Redux or Context Api?</label>
<select ref={stateManagementQRef} name="state-management">
<option value="Redux">Redux</option>
<option value="Context Api">Context Api</option>
</select>
<br></br>
<label>Backend, Frontend, Mobile?</label>
<select ref={programmerTypeQRef} name="profession">
<option value="Back-end">back-end</option>
<option value="Front-end">front-end</option>
<option value="mobile">mobile</option>
</select>
<br></br>
<button type="submit">submit</button>
</form>
</React.Fragment>
)}
</div>
);
};
export default SurveyBox;
Setting the state is an async action, and the updated state would only be available at the next render.
In your case, the default state is undefined, and this is what you send at the 1st submit. The state is now updated, and when you submit again, you send the previous answer, and so on...
To solve this, prepare a const (newAnswer), and set it to the state, and use it in the api call.
Note: in your case, you're not using the surveyUserAnswers at all, so you can remove this state entirely.
const onSubmitSurvey = (e: React.FormEvent): void => {
e.preventDefault();
setLoading((prevLoading) => !prevLoading);
const newAnswer = {
id: Math.random(),
answers: [
programmerTypeQRef.current!.value,
skillsQRef.current!.value,
stateManagementQRef.current!.value,
programmerTypeQRef.current!.value,
],
}
setSurveyUserAnswers(newAnswer);
axios
.post(`${DB_URL}/users-answers.json`, newAnswer)
.then((res) => {
setLoading((prevLoading) => !prevLoading);
})
.catch((error) => {
console.log(error);
setLoading((prevLoading) => !prevLoading);
});
};

Option values inside a select with React

I'm noobie on React and I want to fill a <select>
The problem that I have is when I want to click one of my items... dropdown only show different options if I put my items harcoded. Here's my code
Parent component
import React, { Fragment, useState, useEffect } from "react";
import Country from "./Country";
const CountriesList = ({ handleOnChange }) => {
const [countriesLoaded, setCountriesLoaded] = useState(false);
const [countriesList, setCountriesList] = useState([]);
const getCountries = async () => {
const api = "https://restcountries.eu/rest/v2/all";
const response = await fetch(api);
const countrieslst = await response.json();
setCountriesList(countrieslst);
setCountriesLoaded(true);
};
useEffect(() => {
getCountries();
}, [countriesList]);
return (
<Fragment>
<select id="country" name="country" onChange={handleOnChange}>
<option value="">-- Select a country --</option>
{countriesLoaded
? countriesList.map((country) => (
<Country key={country.alpha2Code} countryItem={country} />
))
: null}
</select>
</Fragment>
);
};
export default CountriesList;
child component
import React from "react";
const Country = ({ country }) => {
return <option value={country.alpha2Code}>{country.name}</option>;
};
export default Country;
If I check DOM it's ok but only shows my first option
Thnx 4 support and have a nice day!
[Edit]
just add <select id="country" name="country" onChange={handleOnChange} style={{display: 'block'}}> in your code. you will be able to view a select box rendering data
here is the link i made few changes in your code as well but this is optional:
https://codesandbox.io/s/nd58i?file=/src/components/Form.jsx

Get currency rates based on currency selection

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

Resources