I understand destructuring that it unpacks values from array into variables and using square brackets in useState means it is returning two values in that array, first one is value and second one is function that is used to change that value,
But I see [] when using custom hook, and It does not make any sense to me , Can anyone please explain me why variable [breeds] has square brackets around it. it is declared under useState.
const ANIMALS = ["bird", "cat", "dog", "rabbit", "reptile"];
export const SearchParams = () => {
const [location, setLocation] = useState("");
const [animal, setAnimal] = useState('');
const [breed, setBreed] = useState('')
const [pets, setPets] = useState([]);
const [breeds] = useBreedList(animal);
useEffect(() => {
requestPets();
},[animal])
async function requestPets() {
const res = await fetch(
`http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}`
);
const json = await res.json();
console.info('json is '+JSON.stringify(json))
setPets(json.pets);
console.warn("pets are " + JSON.stringify(pets));
}
return (
<div className="search-params">
<form>
<label htmlFor="location">
Location
<input
id="location"
value={location}
placeholder="Location"
onChange={(e) => setLocation(e.target.value)}
/>
</label>
<label htmlFor="animal">
Animal
<select
id="animal"
value={animal}
onChange={(e) => {
setAnimal(e.target.value);
setBreed("");
}}
onBlur={(e) => {
setAnimal(e.target.value);
setBreed("");
}}
>
<option />
{ANIMALS.map((animal) => (
<option key={animal}>{animal}</option>
))}
</select>
</label>
<label htmlFor="breed">
Breed
<select
id="breed"
value={breed}
onChange={(e) => setBreed(e.target.value)}
disabled={breeds.length === 0}
>
<option />
{breeds.map((breed) => (
<option key={breed}>{breed}</option>
))}
</select>
</label>
<button>Submit</button>
</form>
{pets.map((pet) => (
<Pet
name={pet.name}
animal={pet.animal}
breed={pet.breed}
key={pet.id}
/>
))}
</div>
);
}
This is custom hook
const localCache = {};
export const useBreedList = (animal) => {
const [breedList, setBreedList] = useState([]);
const [status, setStatus] = useState("unloaded");
useEffect(() => {
if (!animal) {
setBreedList([])
} else if (localCache[animal]) {
setBreedList(localCache[animal])
} else {
requestBreedList();
}
async function requestBreedList() {
setBreedList([]);
setStatus("loading");
const res = await fetch(
`https://pets-v2.dev-apis.com/breeds?animal=${animal}`
)
const json = await res.json();
localCache[animal] = json.breeds || []
setBreedList(localCache[animal]);
setStatus("loaded");
}
}, [animal]);
return [breedList, status];
}
Can anyone please explain me why variable [breeds] has square brackets around it. it is declared under useState.
Because it's destructuring the result of calling useBreedList, which (like useState) returns two pieces of information, wrapped up in a tuple (a fixed-length array):
export const useBreedList = (animal) => {
// ...
return [breedList, status]; // <=================================
};
Since they're returning two pieces of information, the people writing useBreedList had the same choice that the people writing useState did:
Return a tuple (fixed-length array) people can destructure into variables they control the names of
Return an object with (say) breedlist and status properties
It's easier to control your own variable names when using the hook when the hook uses a tuple (#1) rather than an object (#2) — and indeed, the code you've quoted is using breeds rather than breedList. If the hook did this:
return { breedList, status };
...then the quoted code would have had to do this to use the name breeds instead:
const { breedList: breeds } = useBreedList(animal);
That said, this hook is really specific, I wouldn't have been surprised to find it written using a (non-array) object instead, and for code using it to consistently use const { breedList, status } = useBreedList(animal); instead.
JavaScript doesn’t support functions that return multiple values. However, you can wrap multiple values into an array or an object and return the array or the object.
Use destructuring assignment syntax to unpack values from the array, or properties from objects.
Related
I am making a front-end UI returning student objects (grades, email, etc.) from an API call. I currently have a filter set up to return objects by names. I need to set up a second filter by tags, which can be added through an input element within each student component returned from .map() the API. I cannot figure out how to set up the filter as the tags are stored within each instance of the student Profile.js component. Can you please help me? Ultimately the UI should return search results from both filters (name && tags)
Snippet from App.js:
function App() {
const [students, setStudents] = useState([])
const [filteredStudents, setFilteredStudents] = useState([])
const [search, setSearch] = useState("")
// Get request and store response in the 'students' state //
useEffect(()=>{
axios.get('Link to the API')
.then(res => {
const result = res.data.students
setStudents(result)
})
},[])
// Filter students by name and store filtered result in 'filteredStudents' //
useEffect(() => {
const searchResult = []
students.map(student => {
const firstName = student.firstName.toLowerCase()
const lastName = student.lastName.toLowerCase()
const fullName = `${firstName}` + ` ${lastName}`
if (fullName.includes(search.toLowerCase())) {
searchResult.push(student)
}
return
})
setFilteredStudents(searchResult)
}, [search])
return (
<div>
<SearchBar
search={search}
onChange={e => setSearch(e.target.value)}
/>
//Second search bar by tag here//
{search.length == 0 &&
//unfiltered students object here
}
{search.length != 0 &&
<div>
{filteredStudents.map(student => (
<Profile
//Some props here//
/>
))}
</div>
}
</div>
)}
Snippet from Profile.js
//bunch of code before this line//
const [tags, setTags] = useState([])
const [tag, setTag] = useState("")
function handleKeyPress(e) {
if(e.key === 'Enter') {
tags.push(tag)
setTag("")
}
}
return(
<div>
//bunch of code before this line//
<Tag
onChange={e => setTag(e.target.value)}
onKeyPress={handleKeyPress}
tags={tags}
tag={tag}
/>
</div>
)
Snippet from Tag.js:
export default function Tag({tag, tags, onChange, onKeyPress}) {
return (
<div>
{tags.length > 0 &&
<div>
{tags.map(tag => (
<span>{tag}</span>
))}
</div>
}
<input
type='text'
value={tag}
placeholder="Add a tag"
key='tag-input'
onKeyPress={onKeyPress}
onChange={onChange}
/>
</div>
)
}
Edit
With your comment I think I now understand what you're trying to do, the images you provided really helped. You want to change the student object whenever a tag is added in the Profile component if I'm not mistaken (again, correct me if I am). That would mean the Profile component needs access to a handler so that whenever a tag is added, it also sets a new students state. It would look like this:
App.js
function App() {
const [students, setStudents] = useState([]);
const [filteredStudents, setFilteredStudents] = useState([]);
const [search, setSearch] = useState("");
const handleTagAdded = (tag, index) => {
setStudents((prevStudents) => {
// We copy object here as the student we're accessing
// is an object, and objects are always stored by reference.
// If we didn't do this, we would be directly mutating
// the student at the index, which is bad practice
const changedStudent = {...prevStudents[index]};
// Check if student has 'tags` and add it if it doesn't.
if (!("tags" in changedStudent)){
changedStudent.tags = [];
}
// Add new tag to array
changedStudent.tags.push(tag);
// Copy array so we can change it
const mutatableStudents = [...prevStudents];
mutatableStudents[index] = changedStudent;
// The state will be set to this array with the student
// at the index we were given changed
return mutatableStudents;
})
}
// Get request and store response in the 'students' state //
useEffect(() => {
axios.get("Link to the API").then((res) => {
const result = res.data.students;
setStudents(result);
});
}, []);
// Filter students by name and tag, then store filtered result in //'filteredStudents'
useEffect(() => {
// Array.filter() is perfect for this situation //
const filteredStudentsByNameAndTag = students.filter((student) => {
const firstName = student.firstName.toLowerCase();
const lastName = student.lastName.toLowerCase();
const fullName = firstName + lastName;
if ("tags" in student){
// You can now do whatever filtering you need to do based on tags
...
}
return fullName.includes(search.toLowerCase()) && yourTagComparison;
});
setFilteredStudents(filteredStudentsByNameAndTag);
}, [search]);
return (
<div>
<SearchBar search={search} onChange={(e) => setSearch(e.target.value)} />
//Second search bar by tag here //
{search.length === 0 &&
// unfiltered students //
}
{search.length !== 0 && (
<div>
{filteredStudents.map((student, index) => (
<Profile
// Some props here //
onTagAdded={handleTagAdded}
// We give the index so Profile adds to the right student
studentIndex={index}
/>
))}
</div>
)}
</div>
);
}
In handleTagAdded, I copy the object at prevStudents[index] because it is a reference. This may sound odd if you don't know what I'm referring to (pun intended). Here is a link to an article explaining it better than I will be able to.
Profile.js
function Profile({ onTagAdded, studentIndex }) {
// Other stuff //
const [tags, setTags] = useState([]);
const [tag, setTag] = useState("");
const handleTagKeyPress = (e) => {
if (e.key === "Enter") {
// Use this instead of tags.push, when changing state you always
// must use the `setState()` function. If the new value depends on the
// previous value, you can pass it a function which gets the
// previous value as an argument like below. It is also bad
// practice to change, or 'mutate' the argument you're given
// so we instead copy it and change that.
setTags((previousTags) => [...previousTags].push(tag));
setTag("");
onTagAdded(tag, studentIndex)
}
};
return (
<div>
// Other stuff
<Tag onChange={(e) => setTag(e.target.value)} onKeyPress={handleTagKeyPress} tags={tags} tag={tag} />
</div>
);
}
Now, each <Profile /> component has its own tags state, but through the use of handleTagAdded(), we can change the student within each profile component based on tags.
Apologies for the confusion in my first answer, I hope this solves your issue!
Old answer
There's a very important concept in React known as "Lifting State". What this means is that if a parent component needs to access the state of a child component, one solution is to 'lift' the state from the child to the parent.
You can read some more about it in the React documentation.
In this example, you need to lift the tag state up from the <Profile /> component to the <App /> component. That way, both search and tag are in the same place and can be compared.
I believe the code below is along the lines of what you want:
App.js
function App() {
const [students, setStudents] = useState([]);
const [filteredStudents, setFilteredStudents] = useState([]);
const [tags, setTags] = useState([]);
const [tag, setTag] = useState("");
const [search, setSearch] = useState("");
const handleTagChange = (e) => setTag(e.target.value);
const handleTagKeyPress = (e) => {
if (e.key === "Enter") {
// Use this instead of tags.push, when changing state you always
// must use the `setState()` function. If the new value depends on the
// previous value, you can pass it a function which gets the
// previous value as an argument like below.
setTags((previousTags) => previousTags.push(tag));
setTag("");
}
};
// Get request and store response in the 'students' state //
useEffect(() => {
axios.get("Link to the API").then((res) => {
const result = res.data.students;
setStudents(result);
});
}, []);
// Filter students by name and tag, then store filtered result in //'filteredStudents'
useEffect(() => {
// Array.filter() is perfect for this situation //
const filteredStudentsByNameAndTag = students.filter((student) => {
const firstName = student.firstName.toLowerCase();
const lastName = student.lastName.toLowerCase();
const fullName = firstName + lastName;
return fullName.includes(search.toLowerCase()) && student.tag === tag;
});
setFilteredStudents(filteredStudentsByNameAndTag);
}, [search]);
return (
<div>
<SearchBar search={search} onChange={(e) => setSearch(e.target.value)} />
//Second search bar by tag here //
{search.length == 0 &&
// unfiltered students //
}
{search.length != 0 && (
<div>
{filteredStudents.map((student) => (
<Profile
// Some props here //
onChange={handleTagChange}
onKeyPress={handleTagKeyPress}
tag={tag}
tags={tags}
/>
))}
</div>
)}
</div>
);
}
Profile.js
function Profile({ onChange, onKeyPress, tags, tag }) {
// Other stuff //
return (
<div>
// Other stuff
<Tag onChange={onChange} onKeyPress={onKeyPress} tags={tags} tag={tag} />
</div>
);
}
We've moved the tag state up to the <App /> component, so now when we filter we can use both the search and tag. I also changed students.map to students.filter as it is a better alternative for filtering an array.
I'm not clear on how you wanted to filter the tags, so I assumed the student object would have a tag attribute. Feel free to correct me about how the data is structured and I'll reformat it.
I hope this helped, let me know if you have any more problems.
I can succesfully get the data from my database , but how can I display it Inside the Select?
This is where i want to display my data inside the select
<Select
fullWidth
value={newUsers}
onChange={handleChange}
>
<MenuItem value={0}>{newUsers.label}</MenuItem>
this is how i call the data from the database (this is working and I can see the data from my console)
const [newUserLists, setNewUserLists] =useState([]);
const newUsers= [];
const fetchData = async () => {
const response = await getreason();
response.data.map(function(u){
newUsers.push({
label: u.applicationcode,
value: u.applicationid
})
})
setNewUserLists(newUsers)
}
useEffect(() => {
fetchData();
}, [reRender]);
this is the result in console.log
Let's model this on the Material-UI Select tutorial and API doc page. There, we can see a few things:
value is the currently selected input value. It appears you're trying to set it to the list of objects that you read from the DB response, which is not correct. It is common practice to say something like the following:
const [selected, setSelected] = useState("");
const handleChange = (event) => {
setSelected(event.target.value);
};
return (
<FormControl>
<InputLabel>Selection</InputLabel>
<Select
value={selected}
onChange={handleChange}
>
{/* MenuItems go here */}
</Select>
</FormControl>
);
On an unrelated (to Material-UI) note, you seem to not actually use newUserLists in your code. I'm going to make the assumption that newUsers is just a placeholder to munge the data from the response into, but per #Naim-Mustafa's answer, it isn't needed and should not be used in your display.
Now, how do we use newUsersList to generate a list of MenuItems? Just as you've done elsewhere, map is the key:
{newUsersList.map(({ label, value }) => (
<MenuItem id={value} value={value}>
{label}
</MenuItem>
))}
Note that I'm assuming value is unique for the purposes of the id field. If it is not, replace with something more appropriate.
You can change const newUsers= []; to let newUsers= [];
but you dont have to use newUsers array at all, instead try using the state
const [newUserLists, setNewUserLists] =useState([]);
const [selectedOption, setSelectedOption] = useState();
const fetchData = async () => {
const response = await getreason();
setNewUserLists(response.data.map(user => ({
label: user.applicationcode,
value: user.applicationid})))
}
useEffect(() => {
fetchData();
}, [reRender]);
return (
<>
<select value={selectedOption}
onChange={(event) => setSelectedOption(event.target.value)}
>
{newUserLists
.map(user =>
<option key={user.label} value={user.label}>
{user.label}</option>}
</select>
</>
i don't know how make this guys, i can't update my state with the api array, and if i put it in useEffect i have an error cause i am not sending any data, help me please is my first time using stackoverflow
import React, { useEffect, useState } from "react";
import getTeam from "../Helpers/getTeam";
const selectTeams = [
"Barcelona",
"Real Madrid",
"Juventus",
"Milan",
"Liverpool",
"Arsenal",
];
const Select = () => {
const [team, setTeam] = useState(null);
const [loading, setLoading] = useState(null);
const handleOption = async (e) => {
setLoading(true);
let teamsJson = await getTeam(e.target.value);
let arr = [];
Object.keys(teamsJson).map((teamjs, i) => {
return arr.push(teamsJson[teamjs]);
});
console.log(arr);
console.log(team);
setTeam(arr);
setLoading(false);
};
return (
<div
style={{ background: "skyblue", textAlign: "center", padding: "20px" }}
>
<h1>Equipos Disponibles</h1>
<div>
<select onChange={handleOption}>
<option>Elige tu equipo</option>
{selectTeams.map((selectTeam, i) => {
return <option key={i}>{selectTeam}</option>;
})}
</select>
</div>
{loading ? <h1>suave</h1> : (
team !== null ? (
team.map((newTeam, i) => {
return (
<div>
the items are here
</div>
)
})
) : null
)}
</div>
);
};
export default Select;
i let you my api file down
const getTeam = async (teamName) => {
const url = `https://www.thesportsdb.com/api/v1/json/1/searchteams.php?t=${teamName}`;
const res = await fetch(url);
const team = await res.json();
return team;
};
export default getTeam;
i wanna update my const team with the response of my api call, but it doesn't update it, i dont know what do, please help me
The teamsJson value is an object with a single key and value of some array
{ teams: [...] }
So you are updating your state with a nested array when you push the value into another array.
let arr = [];
Object.keys(teamsJson).map((teamjs, i) => {
return arr.push(teamsJson[teamjs]);
});
Based upon how you want to map your team state array I assume you just want the raw inner array from teamJson.
const { teams } = await getTeam(e.target.value);
setTeam(teams);
Then when you are mapping you can access any of the properties you need.
team.map((newTeam, i) => {
return <div key={i}>{newTeam.idTeam}</div>;
})
I've just tested it & it seems to works just fine.
The only 2 issues seem to be that:
You don't use team anywhere (apart from a console.log statement).
At the moment when you console.log(team); the constant team will (yet) be null for the first time (because it still keeps the initial state).
Here's what I see in React dev tools after picking a random team in the <select>:
I'm pretty new to using hooks and functional components.
I have a Filtered List. When I try to update the filter, it will use the last filter state instead of the new one. I must be missing some render/state change orders, but I can't seem to figure out what it is.
I appreciate any help I can get :)
Pseudo code below:
export default function TransferList(props) {
const [wholeList, setWholeList] = React.useState([]);
const [filteredList, setFilteredList] = React.useState([]);
const [filter, setFilter] = React.useState([]);
return (
<>
<TextField
value={filter}
onChange={(e) => {
// Set filter input
setFilter(e.target.value);
// Filter the list
let filtered = wholeList.filter(
(item) => item.indexOf(filter) !== -1
);
setFilteredList(filtered);
}}
/>
<List>
{filteredList.map((item) => (
<ListItem>Item: {item}</ListItem>
))}
</List>
</>
);
}
Inside onChange you should use save the value in a constant, filter will not update just after setFilter(filterValue) as this is an async operation.
<TextField
value={filter}
onChange={e => {
const filterValue = e.target.value;
// Set filter input
setFilter(filterValue);
// Filter the list
let filtered = wholeList.filter(item => item.indexOf(filterValue) !== -1);
setFilteredList(filtered);
}}
/>;
State updates are asynchronous and hence the filter state update doesn't reflect immediately afterwords
You must store the new filter values and set the states based on that
export default function TransferList(props) {
const [wholeList, setWholeList] = React.useState([]);
const [filteredList, setFilteredList] = React.useState([]);
const [filter, setFilter] = React.useState([]);
return (
<>
<TextField value={filter} onChange={(e) => {
// Set filter input
const newFilter = e.target.value;
setFilter(newFilter)
// Filter the list
let filtered = wholeList.filter(item => item.indexOf(newFilter) !== -1)
setFilteredList(filtered)
}} />
<List>
{filteredList.map(item => <ListItem>Item: {item}</ListItem>)}
</List>
</>
)
}
So it turns out I had to give the initial filtered list the entire unfiltered list first. For some reason that fixes it.
const [filteredList, setFilteredList] = React.useState(props.wholeList);
I initially wanted an empty filter to display nothing, but I may have to settle for showing the entire list when the filter is empty
Order your code to be increase readability
In clean code
Main changes:
Use destructuring instead of props
Out the jsx from the html return to increase readability
Use includes instead of indexOf
Add key to the list
export default function TransferList({ wholeList }) {
const [filteredList, setFilteredList] = React.useState(wholeList);
const [filter, setFilter] = React.useState([]);
const handleOnChange = ({ target }) => {
setFilter(target.value);
const updatedFilteredList = wholeList.filter(item => item.includes(target.value));
setFilteredList(updatedFilteredList);
}
const list = filteredList.map(item => {
return <ListItem key={item}>Item: {item}</ListItem>
});
return (
<>
<TextField value={filter} onChange={handleOnChange} />
<List>{list}</List>
</>
);
}
and I have a filter component the do the filter list inside.
import React, { useState } from 'react';
function FilterInput({ list = [], filterKeys = [], placeholder = 'Search',
onFilter }) {
const [filterValue, setFilterValue] = useState('');
const updateFilterValue = ev => {
setFilterValue(ev.target.value);
const value = ev.target.value.toLowerCase();
if (!value) onFilter(list);
else {
const filteredList = list.filter(item => filterKeys.some(key => item[key].toLowerCase().includes(value)));
onFilter(filteredList);
}
}
return (
<input className="filter-input" type="text" placeholder={placeholder}
value={filterValue} onChange={updateFilterValue} />
);
}
export default FilterInput;
the call from the father component look like this
<FilterInput list={countries} filterKeys={['name']} placeholder="Search Country"
onFilter={filterCountries} />
this is in my corona app.. you can look on my Github.
and see how I build the filter
(https://github.com/omergal99/hello-corona)
I'm studying the reaction, and I'm trying to filter by manufacturer the products that I previously request via the API. With this code, products are displayed, and then you can filter by manufacturer. However, when you try to filter them again, i.e. select products from a different manufacturer, nothing happens.
My code:
import React, {useEffect} from 'react'
import Loader from './../Loader/Loader'
import ProductItem from './ProductItem'
function Catalog() {
const [products, setProducts] = React.useState([])
const [brands, setBrands] = React.useState([])
const [brand, setBrand] = React.useState('')
useEffect(() => {
const proxyUrl = 'https://cors-anywhere.herokuapp.com/'
const url = 'https://avtodoka-msk.ru/aimylogic-mission.json'
fetch(proxyUrl + url)
.then(response => response.json())
.then(products => {
setProducts(products)
const brands = []
products.map(product => {
return(
brands.push(...product.brend)
)
})
setBrands([...new Set(brands)])
})
}, [])
function toggleBrand(e) {
setBrand(e.target.value)
setProducts(
products.filter(product => {
if ([...product.brend].includes(e.target.value)) {
return true
}
})
)
}
return (
<div className="container pt-5">
{brands.length ? (
<div className="row">
<div className="col-3">
<select value={brand} onChange={toggleBrand}>
<option value={''}>Выбрать бренд</option>
{brands.map((brend,index) => {
return(
<option value={brend} key={index}>{brend}</option>
)
})}
</select>
</div>
</div>
): null}
{products.length ? (
<div className="row">
{products.map(product => {
return (
<ProductItem
key={product.id}
product={product}
/>
)
})}
</div>
) : <Loader />}
</div>
)
}
export default Catalog
If I understood correctly your state needs to have:
products array - containing your products.
brands array - containing your manufacturers.
brand array - containing an active filter by manufacturer.
If that is correct, then I see a couple of problems with your code:
You're overcomplicating your array methods, leading to wanted behaviors.
Now, the real problem here is that when you filter the first time, you lose access to all the products, and therefore you need to re-fetch them on every filter click.
Giving that, here's your updated code:
import React, {useEffect} from 'react'
import Loader from './../Loader/Loader'
import ProductItem from './ProductItem'
function Catalog() {
const [products, setProducts] = React.useState([])
const [brands, setBrands] = React.useState([])
const [brand, setBrand] = React.useState('')
useEffect(() => {
const proxyUrl = 'https://cors-anywhere.herokuapp.com/'
const url = 'https://avtodoka-msk.ru/aimylogic-mission.json'
fetch(proxyUrl + URL)
.then(response => response.json())
.then(products => {
if (brands.length <= 0)
setBrandsFromProducs(products)
const filteredProducts = products.filter(product => !brand || product.brend.includes(brand))
setProducts(filteredProducts)
})
}, [brand])
function setBrandsFromProducs(products) {
const brands = products.reduce(
(acc, product) => ([...acc, ...product.brend])
, [])
setBrands([...new Set(brands)])
}
function toggleBrand(e) {
const selectedBrand = e.target.value
setBrand(selectedBrand)
}
}
Key aspects:
product.brend is an array. To extract all the brands from this JSON, you need to flatten an array of arrays, therefore you're reducing it into a flat array - hence the usage of a .reduce(). You only need to do this if the brands array is empty. However, if you're not concerned about performance you can remove the if statement to always reset the brandsarray.
Your event is selecting the brand, it shouldn't contain any other side-effect (filtering the products). Those should live inside a proper scope -> useEffect. To trigger this event, you need to 'listen' to changes to the brand filter, therefore it needs to be specified as a dependency of the useEffect.
Your filter was missing the return false outside the if statement. However, it could be simplified because .includes() returns a boolean value and doesn't mutate the original array. The filter I created will first check if you have an active filter and only filter if you do have one.
You need to make sure that e.target.value is the exact string from the product.brend's array.
Small suggestions:
If you control the API, your filters should always be on the backend side to avoid bigger payloads than you need.
If you don't use the brand state, you can safely remove it. Without your JSX I can't be sure.