I am trying to implement select-option in React using custom hooks and encountered an issue while trying to set a default value in select option. From the fetched data in UI, that comes from web API, I was able to show selected data based on category(in my case it's cuisine). But when I select default value to show All data, state doesn't update.
Another problem is about the duplicated values in select option. I need to have unique values as option values. I was thinking about to get unique values this way
<option key={restaurant.id}>{[...new Set(restaurant.cuisine)]}</option>
But this removes duplicated characters,but not the duplicated values.
Code below.
Hooks/useRestaurants component
import React, { useState, useEffect } from "react";
const useRestaurants = (cuisine) => {
const [allRestaurants, setAllRestaurants] = useState([]);
useEffect(() => {
fetch("https://redi-final-restaurants.herokuapp.com/restaurants")
.then((res) => res.json())
.then((result) => setAllRestaurants(result.results))
.catch((e) => console.log("error"));
}, []);
useEffect(() => {
if (cuisine === "All") {
const filterRestaurants = [...allRestaurants].filter((restaurant) => // here is my try
restaurant.cuisine.toLowerCase().includes(cuisine.toLowerCase())//code here doesn't work
);
setAllRestaurants(filterRestaurants);
} else {
const filterRestaurants = [...allRestaurants].filter((restaurant) =>
restaurant.cuisine.toLowerCase().includes(cuisine.toLowerCase())
);
setAllRestaurants(filterRestaurants);
}
}, [cuisine]);
return [allRestaurants];
};
export default useRestaurants;
App.js component
import React, { useState } from "react";
import useRestaurants from "./useRestaurants";
import Form from "./Form";
import Restaurant from "./Restaurant";
import "./styles.css";
export default function App() {
const [cuisine, setCuisine] = useState("All");
const [allRestaurants] = useRestaurants(cuisine);
const onChangeHandler = (e) => {
setCuisine(e.target.value);
};
return (
<div className="App">
<Form
onChangeHandler={onChangeHandler}
allRestaurants={allRestaurants}
cuisine={cuisine}
setCuisine={setCuisine}
/>
{allRestaurants &&
allRestaurants.map((restaurant) => (
<Restaurant restaurant={restaurant} key={restaurant.id} />
))}
</div>
);
}
And Form.js component
import React from "react";
const Form = ({ allRestaurants, cuisine, onChangeHandler }) => {
return (
<select onChange={onChangeHandler} value={cuisine}>
<option value={cuisine}>All</option>
{allRestaurants.map((restaurant) => (
<option key={restaurant.id}>{restaurant.cuisine}</option>
))}
</select>
);
};
export default Form;
Any help will be appreciated.
The useEffect in useRestaurants that is performing the filtering is missing allRestaurants from the dependency array. This means that the initial value (an empty array) will always be used within that useEffect. Thus, changing the cuisine will set allRestaurants to an empty array. However, you can't add allRestaurants to the dependency array and set it from within the effect. That will cause it to loop infinitely. The solution is to not use an effect - just create the filtered result and return it either as a separate value or in place of allRestaurants
// useRestaurants.js
import { useState, useMemo, useEffect } from "react";
const useRestaurants = (cuisine) => {
const [allRestaurants, setAllRestaurants] = useState([]);
useEffect(() => {
fetch("https://redi-final-restaurants.herokuapp.com/restaurants")
.then((res) => res.json())
.then((result) => setAllRestaurants(result.results))
.catch((e) => console.log("error"));
}, []);
const filteredRestaurants = useMemo(() => {
return cuisine === "All"
? allRestaurants
: allRestaurants.filter((restaurant) =>
restaurant.cuisine.toLowerCase().includes(cuisine.toLowerCase())
);
}, [cuisine, allRestaurants]);
return [allRestaurants, filteredRestaurants];
};
export default useRestaurants;
To fix the duplicate cuisine values you need to create the Set and then filter over that result. Your form is still filtering over all allRestaurants and {[...new Set(restaurant.cuisine)]} is just creating an array with a single value.
// Form.js
import React from "react";
const Form = ({ allRestaurants, cuisine, onChangeHandler }) => {
const cuisines = Array.from(new Set(allRestaurants.map((r) => r.cuisine)));
return (
<select onChange={onChangeHandler} value={cuisine}>
<option value='All'}>All</option>
{cuisines.map((cuisine) => (
<option id={cuisine}>{cuisine}</option>
))}
</select>
);
};
export default Form;
Remember to loop over the filtered restaurants in App.js
...
const [allRestaurants, filteredRestaurants] = useRestaurants(cuisine);
...
return (
...
{filteredRestaurants &&
filteredRestaurants.map((restaurant) => (
<Restaurant restaurant={restaurant} key={restaurant.id} />
))}
)
Related
I created a converter of currency and I have a warning in console
Warning: The value prop supplied to <select> must be a scalar value if multiple is false.
Check the render method of Row.
at select
at div
at Row (http://localhost:3000/static/js/main.chunk.js:930:5)
at form
at div
at App (http://localhost:3000/static/js/main.chunk.js:192:101)
Using multiple=true isn't ok for me because it shows a few options.
Everything works well exept this warning. How I could solve it?
App.js
import React, {useState, useEffect} from 'react';
import './App.css';
import Row from './row/Row'
const App = () => {
const [selectCurrency, setSelectCurrency] = useState()
const [fromCurrency, setFromCurrency] = useState()
const [toCurrency, setToCurrency] = useState([])
const [amount, setAmount] = useState(1)
const [amountCurrency, setAmountCurrency] = useState(true)
const [exchangeRate, setExchangeRate] = useState()
let toAmount, fromAmount
if(amountCurrency){
fromAmount = amount
toAmount = amount * exchangeRate
} else{
toAmount = amount
fromAmount = amount / exchangeRate
}
useEffect(() => {
fetch(`https://api.exchangerate.host/latest`)
.then(res => res.json())
.then(data => {
setSelectCurrency([data.base, ...Object.keys(data.rates)])
setFromCurrency(data.base)
setToCurrency(Object.keys(data.rates)[0])
setExchangeRate(data.rates[Object.keys(data.rates)[0]])
})
}, [])
useEffect(() => {
if(fromCurrency && toCurrency){
fetch(`https://api.exchangerate.host/convert?from=${fromCurrency}&to=${toCurrency}`)
.then(res => res.json())
.then(res => setExchangeRate(res.info.rate))
}
}, [fromCurrency, toCurrency])
const onFromChangeAmount = (e) => {
setAmount(e.target.value)
setAmountCurrency(true)
}
const onToChangeAmount = (e) => {
setAmount(e.target.value)
setAmountCurrency(false)
}
return(
<div>
<form >
<Row
selectCurrency={selectCurrency}
currency={fromCurrency}
onChangeCurrency={e => setFromCurrency(e.target.value)}
amount={fromAmount}
onChangeAmount={onFromChangeAmount}
/>
<Row
selectCurrency={selectCurrency}
currency={toCurrency}
onChangeCurrency={e => setToCurrency(e.target.value)}
amount={toAmount}
onChangeAmount={onToChangeAmount}
/>
</form>
</div>
)
}
export default App;
Row.js
import React from 'react';
function Row(props){
const {
currency,
name,
selectCurrency,
onChangeCurrency,
amount,
onChangeAmount
} = props
return(
<div id={name}>
<input type="number"
className="inp"
value={amount ? amount : ''}
onChange={onChangeAmount}
/>
<select value={currency}
onChange={onChangeCurrency}
>
{selectCurrency && selectCurrency.map((el, i) => {
return <option key={i}>{el}</option>
})}
</select>
</div>
)
}
export default Row;
I've found my mistake: in prop toCurrency I was passed empty array to Row.js.
I am building an application that has a database of videos that can be filtered by category and sorted by rating.
Filtering works after changing the options. However, when I change the categories of the video the filtering does not start automatically. I added useEffect but I don't know what else I can change and why it happens. Please help how to make the sorting not disappear when changing the cateogry.
UPDATE:
import * as _ from "lodash";
import { useEffect, useState } from "react";
import { getAllPrograms } from "../../helpers/getData";
import { TVProgram } from "../../models/models";
import Filters from "../Filters/Filters";
import ProgramsList from "../ProgramsList/ProgramsList";
import Sorting from "../Sorting/Sorting";
import "./HomePage.scss";
const HomePage = () => {
const [programs, setPrograms] = useState<Array<TVProgram>>([]);
const [category, setCategory] = useState<string>("movie,series");
const [sortedPrograms, setSortedPrograms] = useState<TVProgram[]>(programs);
const getPrograms = async (category: string) => {
const programs = await getAllPrograms(category);
setPrograms(programs);
};
useEffect(() => {
getPrograms(category);
}, [category]);
const updateCategory = (categoryName: string): void => {
setCategory(categoryName);
console.log("catName", categoryName);
};
const updatePrograms = (sortedPrograms: TVProgram[]): void => {
setSortedPrograms(sortedPrograms);
console.log("sortedPrograms", sortedPrograms);
};
return (
<div className="container">
<div>
<Filters
updateCategory={updateCategory}
currentCategory={category}
></Filters>
<Sorting programs={programs} setPrograms={updatePrograms}></Sorting>
</div>
<ProgramsList programs={sortedPrograms}></ProgramsList>
</div>
);
};
export default HomePage;
import _ from "lodash";
import { ChangeEvent, useEffect, useState } from "react";
import { sortProgramsByOrder } from "../../helpers/helpers";
import { TVProgram } from "../../models/models";
import "./Sorting.scss";
interface SortingListProps {
programs: TVProgram[];
setPrograms: (programs: TVProgram[]) => void;
}
const Sorting = ({ programs, setPrograms }: SortingListProps) => {
const OPTIONS = ["imdb rating descending", "imdb rating ascending"];
const [selectedOption, setSelectedOption] = useState<string>("");
const [sortedPrograms, setSortedPrograms] = useState<TVProgram[]>([]);
useEffect(() => {
if (selectedOption === OPTIONS[0]) {
setSortedPrograms(sortProgramsByOrder(programs, "desc"));
} else if (selectedOption === OPTIONS[1]) {
setSortedPrograms(sortProgramsByOrder(programs, "asc"));
}
}, [selectedOption, programs]);
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
console.log();
setSelectedOption(event.target.value);
setPrograms(sortedPrograms);
};
return (
<div>
<select value={selectedOption} onChange={handleChange}>
<option selected>Sortuj</option>
{OPTIONS.map((option) => (
<option
key={option}
value={option}
selected={option === selectedOption}
>
{option}
</option>
))}
</select>
</div>
);
};
export default Sorting;
useEffect() is a hook that prevents updates to a variable except in specific cases. Any variable passed into the array at the end of the useEffect() hook will cause the code inside to be run again when its value changes. The problem looks, at first glance, to be in the following part of your code:
useEffect(() => {
if (selectedOption === OPTIONS[0]) {
sortPrograms(sortProgramsByOrder(programs, "desc"));
} else if (selectedOption === OPTIONS[1]) {
sortPrograms(sortProgramsByOrder(programs, "asc"));
}
}, [selectedOption]);
The [selectedOption] is telling the hook to only do the sorting if the sorting order has changed. However, you want to call this hook if the order or the contents changes. As such, you want to replace this array with [selectedOption, programs] so that changes to the contents of the programs variable will also lead to the sorting being re-run.
If programs is updated in the hook and also set by the hook, this leads to a recursive call which is not good. Instead, let's change the displayed value to be a new variable (defined with useState) called sortedPrograms. Then your hook should look like this:
useEffect(() => {
if (selectedOption === OPTIONS[0]) {
setSortedPrograms(sortProgramsByOrder(programs, "desc"));
} else if (selectedOption === OPTIONS[1]) {
setSortedPrograms(sortProgramsByOrder(programs, "asc"));
}
}, [selectedOption, programs]);
I am new to react and creating my first react app. not sure why the todo list is not saved even though I have used localStorage set and get methods. I am also getting error about the key in my map method. I can't seen to find any issues on my own with the code.Below is the code of the todo list App
import TodoList from "./TodoList";
import {v4 as uuid} from 'uuid'
function App() {
const [todos,setTodos] = useState([{}]);
const inputRef = useRef();
const LOCAL_STORAGE_KEY = "todoapp"
useEffect(() =>{
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if(storedTodos){
setTodos(storedTodos)}
}, [])
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
}, [todos])
function toggleTodo(id){
const newTodos= [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
setTodos(newTodos)
}
function handleAdd(e) {
const name = inputRef.current.value;
if(name === "")return
setTodos(prevTodos => {
return [...prevTodos,{id:uuid(),name:name,complete:false}]
})
inputRef.current.value = null;
}
function handleClearTodos(){
const newTodos = todos.filter(todo=>!todo.complete)
setTodos(newTodos)
}
return (
<>
<h1>Chores!!</h1>
<TodoList todo={todos} toggleTodo ={toggleTodo} />
<input ref={inputRef} type="text" />
<button onClick ={handleAdd}>Add todo</button>
<button onClick={handleClearTodos}>Clear todo </button>
<div> {todos.filter(todo => !todo.complete).length} left todo</div>
</>
)
}
export default App;
import Todo from './Todo'
export default function TodoList({todo,toggleTodo}) {
return (
todo.map((todo)=> {
return <Todo key={todo.id} todo={todo} toggleTodo={toggleTodo} />
})
)
}
This:
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
}, [todos])
Is probably taking the initial state of todos on the first render (empty array) and overwriting what data was in their with that initial state.
You might think the previous effect counters this since todos is populated from local storage -- but it doesn't, because on that initial render pass, the second effect will only see the old value of todos. This seems counter-intuitive at first. But it's because whenever you call a set state operation, it doesn't actual change the value of todos immediately, it waits until the render passes, and then it changes for the next render. I.e. it is, in a way, "queued".
For the local storage setItem, you probably want to do it in the event handler of what manipulates the todos and not in an effect. See the React docs.
import TodoList from "./TodoList";
import {v4 as uuid} from 'uuid'
function App() {
const [todos,setTodos] = useState([{}]);
const inputRef = useRef();
const LOCAL_STORAGE_KEY = "todoapp"
const storeTodos = (todos) => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
setTodos(todos)
}
useEffect(() =>{
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if(storedTodos){
setTodos(storedTodos)}
}, [])
function toggleTodo(id){
const newTodos= [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
storeTodos(newTodos)
}
function handleAdd(e) {
const name = inputRef.current.value;
if(name === "")return
storeTodos(prevTodos => {
return [...prevTodos,{id:uuid(),name:name,complete:false}]
})
inputRef.current.value = null;
}
function handleClearTodos(){
const newTodos = todos.filter(todo=>!todo.complete)
storeTodos(newTodos)
}
return (
<>
<h1>Chores!!</h1>
<TodoList todo={todos} toggleTodo ={toggleTodo} />
<input ref={inputRef} type="text" />
<button onClick ={handleAdd}>Add todo</button>
<button onClick={handleClearTodos}>Clear todo </button>
<div> {todos.filter(todo => !todo.complete).length} left todo</div>
</>
)
}
export default App;
As the for the key error, we'd need to see the code in TodoList, but you need to ensure when you map over them, that the id property of each todo is passed to a key prop on the top most element/component within the map callback.
I basically try to update filter the items from the all locations array to dropDownLocation array, but it is not updating correctly. on the first change in input field it wrongly update the array and the second change it does not update it.
import logo from "./logo.svg";
import "./App.css";
import { useState, useEffect } from "react";
function App() {
// location entered by the user
const [location, setlocation] = useState("");
const [allLocations, setallLocations] = useState(["F-1", "F-2", "G-1", "G-2"]);
const [dropDownLocations, setdropDownLocations] = useState([]);
const filterLocations = (userInput) => {
console.log("user input ", location);
allLocations.map((i) => {
if (i.includes(location)) {
console.log("true at ", i);
setdropDownLocations([...dropDownLocations, i]);
} else {
setdropDownLocations([]);
}
});
console.log("after map ", dropDownLocations);
};
return (
<div className="App">
<div>
<input
value={location}
onChange={(e) => {
setlocation(e.target.value);
filterLocations(e.target.value);
}}
/>
<ul>
{dropDownLocations.map((i) => (
<li key={i}>{i}</li>
))}
</ul>
</div>
</div>
);
}
export default App;
You don't need to make that complicated, Just filter the array based on the user's input
const filterLocations = (userInput) => {
setdropDownLocations(
allLocations.filter((location) => location.includes(userInput))
);
};
I made it simpler for you in this working example:
The setState is an asynchronous function and so your current implementation isn't working properly as you are trying to read the state before it is updated.
Update your filterLocations function like following:
const filterLocations = (e) => {
const location = e.target.value;
const filteredLocation = allLocations.filter(i => i.includes(location));
setlocation(location);
setdropDownLocations(filteredLocation)
};
And update your input tag like following:
<input value={location} onChange={filterLocations} />
It is not working because for each location, you are setting dropdown location, and if it doesn't contain the location, you set it to empty array [] again.
allLocations.map((i) => {
if (i.includes(location)) {
console.log("true at ", i);
setdropDownLocations([...dropDownLocations, i]);
} else {
setdropDownLocations([]);
}
});
A better approach would be:
setDropDownLocation([...allLocations].filter((i) => i.includes(userInput))
There is some mistakes what you have done, I have made some changes try to run the code which I have written.
import logo from "./logo.svg";
import "./App.css";
import { useState, useEffect } from "react";
const ALL_LOCATIONS = ['F-1', 'F-2', 'G-1', 'G-2'];
function App() {
// location entered by the user
const [location, setLocation] = useState("");
const [dropDownLocations, setDropDownLocations] = useState([]);
function onLocationInputChange(event){
setLocation(event.target.value);
setDropDownLocations(ALL_LOCATIONS.filter((item)=>item.includes(event.target.value)))
}
return (
<div className="App">
<div>
<input value={location} onChange={onLocationInputChange} />
<ul>
{dropDownLocations.map((loc) => (
<li key={`${location}-${loc}`}>{loc}</li>
))}
</ul>
</div>
</div>
);
}
export default App;
This is caused by the fact that your onChange handler is defined right in the JSX, causing React to recreate a new function at every render (same goes for filterLocations one).
You should always try to extract every single piece of JS logic outside of the component, or at least memoize them, here's how:
import React, { useState, useCallback } from "react";
import logo from "./logo.svg";
import "./App.css";
const ALL_LOCATIONS = ['F-1', 'F-2', 'G-1', 'G-2'];
function App() {
// location entered by the user
const [location, setLocation] = useState("");
// locations shown to the user in dropdown (filterable)
const [dropDownLocations, setDropDownLocations] = useState([]);
const onLocationInputChange = useCallback(
(ev) => {
// In case no target passed to callback, do nothing
if (!ev || !ev.target || !ev.target.value) {
return;
}
const userInput = ev.target.value;
// Filter so that if user input matches part of the location
// it gets not filtered out
setDropDownLocations([
...ALL_LOCATIONS.filter(
(loc) =>
loc.startsWith(userInput) ||
loc.endsWith(userInput) ||
loc.indexOf(userInput) !== -1
),
]);
// Finally update the location var
setLocation(userInput);
},
[setDropDownLocations]
);
return (
<div className="App">
<div>
<input value={location} onChange={onLocationInputChange} />
<ul>
{dropDownLocations.map((loc) => (
<li key={`${location}-${loc}`}>{loc}</li>
))}
</ul>
</div>
</div>
);
}
export default App;
I am building a search component where the user types in the value and it filters through some dummy data and renders the result for the user to see. The problem is when I type in one character in the search field i get the entire array of data on every character I type. So for example there are 4 data strings in the array so if I type in two characters in the search bar then my result is 8.
Here is the code and the problem duplicated: Any help is appreciated.
The answer is below, basically, the code was loading in an extra map from CompanyInfoList component. I also had a useContext that was not needed in the same component, so i removed it and replaced results.map from useContext setState hook to filtered.map. filtered was the prop that needed to be passed down from CompSearch to CompanyInfoList. The last change I made was to delete the RenderList component and remove return RenderList in the serachList component to CompanyInfoList
CompSearch.js
import React, {useContext, useState} from "react";
import CompanyInfoList from "./CompanyInfoList";
import { CompListContext } from "./CompListContext";
const CompSerach = () => {
// const [input, setInput] = useState('');
const [results, setResults] = useContext(CompListContext);
const [searchField, setSearchField] = useState("");
const [searchShow, setSearchShow] = useState(false);
const filtered = results.filter((res) => {
return (
res.name.toLowerCase().includes(searchField.toLowerCase()) ||
res.employee.toLowerCase().includes(searchField.toLowerCase()) ||
res.date.toLowerCase().includes(searchField.toLowerCase()) ||
res.amount.toLowerCase().includes(searchField.toLowerCase())
);
});
const handleChange = (e) => {
setSearchField(e.target.value);
if (e.target.value === "") {
setSearchShow(false);
} else {
setSearchShow(true);
}
};
function searchList() {
if (searchShow) {
return <CompanyInfoList filtered={filtered} />;
}
}
return (
<>
<div>
<input
type="search"
placeholder="search Company Title"
// input="input"
// value={input}
onChange={handleChange}
// onChange={handleChange}
/>
</div>
{searchList()}
</>
);
};
export default CompSerach;
CompanyInfoList.js
import Results from "./Results";
const CompanyInfoList = ({ filtered }) => {
const fltr = filtered.map((result) => (
<Results
key={result.id}
name={result.name}
employee={result.employee}
date={result.date}
amount={result.amount}
/>
));
return <div>{fltr}</div>;
};
export default CompanyInfoList;