So I'm trying fetch data with axios, process it and then render as an option in React select.
I will put bellow sections of my code to make it easier to understand.
const [data1, setData1] = useState([]);
const [data2, setData2] = useState([]);
const [options, setOptions] = useState([]);
useEffect(() => {
axios.get(url, {
.then(response => {
if (response.status == 200) {
setData1(response.data);
}
})
.catch((response) => {
});
axios.get(url, {
.then(response => {
if (response.status == 200) {
setData2(response.data);
}
})
.catch((response) => {
});
data1.map(info1 => {
let y = data2.filter(info2 => {
return info1.id == info2.id
})
let z = y.map(user => {
return { value: user, label: user.username }
});
setOptions(y);
})
}, []);
And this is the rendering
<select value={selectedOption} onChange={handleChange} className='w-40'>
{options.map((option) => {
return (
<option value={option.value}>
{option.label}
</option>)
})}
</select>
But it's just giving me an empty select. I understand that my useEffect will run a single time after the render it's done and I should use async/await somehow but I don't understand where. I used const state [render, setReder] for conditional rendering but still won't work.
How should I use async/await?
Thank you for your time.
Personally, I think you should split your useEffect in two:
const [data1, setData1] = useState([]);
const [data2, setData2] = useState([]);
const [options, setOptions] = useState([]);
useEffect(() => {
axios.get(url, {
.then(response => {
if (response.status == 200) {
setData1(response.data);
}
})
.catch((response) => {
});
axios.get(url, {
.then(response => {
if (response.status == 200) {
setData2(response.data);
}
})
.catch((response) => {
});
}, []);
useEffect(() => {
data1.map(info1 => {
let y = data2.filter(info2 => {
return info1.id == info2.id
})
let z = y.map(user => {
return { value: user, label: user.username }
});
setOptions(z);
});
}, [data1, data2]);
This way, the options are updated whenever data1 or data2 are modified.
Updating options will trigger a re-render of your select options.
From a stylistic point of view, data1.map should probably be data1.forEach and keep in mind that your current loop assumes data1 only contains a single entry.
I don't know what sort of data you're dealing with, but this might be more accurate:
const newOptions = [];
data1.forEach(info1 => {
let y = data2.filter(info2 => {
return info1.id == info2.id
})
let z = y.map(user => {
return { value: user, label: user.username }
});
newOptions.push(...z);
});
setOptions(newOptions);
I think it's better to use another useEffect with data1 and data2 dependencies. So when data1 and data2 change, this function will be called and inside it, you can calculate options.
useEffect(()=>{
data1.map(info1 => {
let y = data2.filter(info2 => {
return info1.id == info2.id
})
let z = y.map(user => {
return { value: user, label: user.username }
});
setOptions(z);
})
}, [data1, data2])
and also in the rendering part, you should check if the options data is available, render it. otherwise, show loading or something else.
return (
{options && options.length === 0 ? (
<Loading />
) : (
<select value={selectedOption} onChange={handleChange} className='w-40'>
{options.map((option) => (
<option value={option.value}>
{option.label}
</option>)
}))
</select>
))
also for parallel request, I think it's better to use promise.all like this example:
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
Promise.all([getUserAccount(), getUserPermissions()])
.then(function (results) {
const acct = results[0];
const perm = results[1];
});
You can use async/await in your code as follows
const [data1, setData1] = useState([]);
const [data2, setData2] = useState([]);
const [options, setOptions] = useState([]);
const fetchData = async() => {
const response1 = axios.get(url)
if (response1.status == 200) {
setData1(response1.data);
}
const response2 = axios.get(url)
if (response2.status == 200) {
setData2(response2.data);
}
}
useEffect(() => {
fetchData();
}, []);
useEffect(()=>{
if(data1.length > 0 && data2.length > 0){
data1.map(info1 => {
let y = data2.filter(info2 => {
return info1.id == info2.id
})
let z = y.map(user => {
return { value: user, label: user.username }
});
setOptions(z);
})
}
},[data1, data2])
And in the render function
{options.length > 0 && <select value={selectedOption} onChange={handleChange}
className='w-40'>
{options.map((option) => {
return (
<option value={option.value}>
{option.label}
</option>)
})}
</select>}
The proper way to do conditional rendering in React is pretty simple.
Usually involves checking some value and basing the render upon the result.
Example:
if (options.length === 0) {
return <></>;
}
return (
<select value={selectedOption} onChange={handleChange} className='w-40'>
{options.map((option) => (
<option value={option.value}>
{option.label}
</option>)
}))
</select>
)
Or it can also be inlined.
Like so:
return (
{options.length === 0 ? (
<></>
) : (
<select value={selectedOption} onChange={handleChange} className='w-40'>
{options.map((option) => (
<option value={option.value}>
{option.label}
</option>)
}))
</select>
))
Note: This example is based on checking the value of options, the same method can be used for other states and variables as-well.
Edit:
Also, what you are trying to achieve here will likely not work since setState in React acts like an async function.
Meaning that by the time you try to map over the value of data1 in the useEffect, data1 has likely not finished updating yet and still holds the previous value.
In order to do this properly in this case you can simply map over the value of response.data instead.
Example:
useEffect(() => {
axios.get(url, {
.then(response => {
if (response.status == 200) {
setData1(response.data);
response.data.map(info1 => {
let y = data2.filter(info2 => {
return info1.id == info2.id
})
// Side-note: I don't understand why `z` is needed here if not used.
let z = y.map(user => {
return { value: user, label: user.username }
});
setOptions(y);
})
}
})
.catch((response) => {
});
axios.get(url, {
.then(response => {
if (response.status == 200) {
setData2(response.data);
}
})
.catch((response) => {
});
}, []);
PS: see if you even need data1 / data2 in this case.
I would suggest to just try and make it a single object, etc..
You can simply "wait" for the data.
(data1.length > 0 && data2.length > 0) ? [render select here] : [render loading spinner/loading text]
Related
This is below code where i am using to fetch values.In country I am managing countries and on the base of country when formik detects there there is change in country and if country Id exists so use effect is called and bind states based on that country if same for states and cities are updated based on stateId not in case of edit e.g. if i am fetching data from API and i am receiving all ids from API the select is not getting binded with default value except countryId as country is independent
const [countries, setCountries] = useState<any>([]);
const [states, setStates] = useState<any>([]);
const [cities, setCities] = useState<any>([]);
const [previousCountryId, setPreviousCountryId] = useState<number | null>(null);
const [previousStateId, setPreviousStateId] = useState<number | null>(null);
const countryIdRef = useRef<number>(formik.values.countryId);
const stateIdRef = useRef<number>(formik.values.stateId);
const bindCountries = () => {
getAllCountries()
.then((res: any) => {
if (res.data.status) {
let data = res.data.data;
setCountries(data);
} else {
setCountries([]);
}
})
.catch(() => {
setCountries([]);
});
};
```
const bindStates = (countryId: number) => {
getStatesByCountry(countryId)
.then((res: any) => {
const array = res.data.data;
setStates(array);
})
.catch((err: any) => {
console.log(err);
});
};
```
```
const bindCities = (stateId: number) => {
getCitiesByState(stateId)
.then((res: any) => {
const array = res.data.data;
setCities(array);
})
.catch((err: any) => {
console.log(err);
});
};
```
```
const handleCountryChange = (value: number) => {
countryIdRef.current = value;
setPreviousCountryId(formik.values.countryId);
formik.setFieldValue('countryId', value);
};
```
```
const handleStateChange = (value: number) => {
stateIdRef.current = value;
setPreviousStateId(formik.values.stateId);
formik.setFieldValue('stateId', value);
};
```
```
useEffect(() => {
bindCountries();
}, []);
```
```
```
useEffect(() => {
if (previousCountryId !== formik.values.countryId && formik.values.countryId !== 0 && formik.values.countryId != undefined) {
setStates([]);
setCities([]);
bindStates(formik.values.countryId);
}
}, [formik.values.countryId, previousCountryId]);
```
```
useEffect(() => {
if (previousStateId !== formik.values.stateId && formik.values.stateId !== 0 && formik.values.stateId != undefined) {
setCities([]);
bindCities(formik.values.stateId);
}
}, [formik.values.stateId, previousStateId]);
```
```
useEffect(() => {
if (tourId == null || tourId == undefined) {
formik.setFieldValue('cityId', 0);
}
}, [tourId]);
```
```
useEffect(() => {
setStates([])
setCities([])
if (tourId != null && tourId != undefined) {
formik.setFieldValue('stateId', 0)
formik.setFieldValue('cityId', 0)
}
if (formik.values.countryId !== 0 && formik.values.countryId!= undefined) {
bindStates(formik.values.countryId)
}
}, [formik.values.countryId])
useEffect(() => {
setCities([])
// if page is edit page then dont set cityId to 0
if (tourId == null || tourId == undefined) {
formik.setFieldValue('cityId', 0)
}
if (formik.values.stateId !== 0 && formik.values.stateId!= undefined) {
bindCities(formik.values.stateId)
}
}, [formik.values.stateId])
<FieldSelect
className='col-lg-6'
isRequired
label='Country'
name='countryId'
formik={formik}
onChange={handleCountryChange}
defaultValue={formik.values.countryId}
>
<option value={0}>Select Country</option>
{countries.map((item: any) => {
return (
<option key={item.id} value={item.id}>
{item.name}
</option>
)
})}
</FieldSelect>
<FieldSelect className='col-lg-6'
onChange={handleStateChange}
defaultValue={formik.values.stateId}
isRequired label='State' name='stateId' formik={formik}>
<option value={0}>Select State</option>
{states.map((item: any) => {
return (
<option key={item.id}
selected={item.id === formik.values.stateId} // <-- set selected prop
value={item.id}>
{item.name}
</option>
)
})}
</FieldSelect>
I have a list and this list has several elements and I iterate over the list. For each list I display two buttons and an input field.
Now I have the following problem: as soon as I write something in a text field, the same value is also entered in the other text fields. However, I only want to change a value in one text field, so the others should not receive this value.
How can I make it so that one text field is for one element and when I write something in this text field, it is not for all the other elements as well?
import React, { useState, useEffect } from 'react'
import axios from 'axios'
function Training({ teamid }) {
const [isTrainingExisting, setIsTrainingExisting] = useState(false);
const [trainingData, setTrainingData] = useState([]);
const [addTraining, setAddTraining] = useState(false);
const [day, setDay] = useState('');
const [from, setFrom] = useState('');
const [until, setUntil] = useState('');
const getTrainingData = () => {
axios
.get(`${process.env.REACT_APP_API_URL}/team/team_training-${teamid}`,
)
.then((res) => {
if (res.status === 200) {
if (typeof res.data !== 'undefined' && res.data.length > 0) {
// the array is defined and has at least one element
setIsTrainingExisting(true)
setTrainingData(res.data)
}
else {
setIsTrainingExisting(false)
}
}
})
.catch((error) => {
//console.log(error);
});
}
useEffect(() => {
getTrainingData();
}, []);
const deleteTraining = (id) => {
axios
.delete(`${process.env.REACT_APP_API_URL}/team/delete/team_training-${teamid}`,
{ data: { trainingsid: `${id}` } })
.then((res) => {
if (res.status === 200) {
var myArray = trainingData.filter(function (obj) {
return obj.trainingsid !== id;
});
//console.log(myArray)
setTrainingData(() => [...myArray]);
}
})
.catch((error) => {
console.log(error);
});
}
const addNewTraining = () => {
setAddTraining(true);
}
const addTrainingNew = () => {
axios
.post(`${process.env.REACT_APP_API_URL}/team/add/team_training-${teamid}`,
{ von: `${from}`, bis: `${until}`, tag: `${day}` })
.then((res) => {
if (res.status === 200) {
setAddTraining(false)
const newTraining = {
trainingsid: res.data,
mannschaftsid: teamid,
von: `${from}`,
bis: `${until}`,
tag: `${day}`
}
setTrainingData(() => [...trainingData, newTraining]);
//console.log(trainingData)
}
})
.catch((error) => {
console.log(error);
});
}
const [editing, setEditing] = useState(null);
const editingTraining = (id) => {
//console.log(id)
setEditing(id);
};
const updateTraining = (trainingsid) => {
}
return (
<div>
{trainingData.map((d, i) => (
<div key={i}>
Trainingszeiten
<input class="input is-normal" type="text" key={ d.trainingsid } value={day} placeholder="Wochentag" onChange={event => setDay(event.target.value)} readOnly={false}></input>
{d.tag} - {d.von} bis {d.bis} Uhr
<button className="button is-danger" onClick={() => deleteTraining(d.trainingsid)}>Löschen</button>
{editing === d.trainingsid ? (
<button className="button is-success" onClick={() => { editingTraining(null); updateTraining(d.trainingsid); }}>Save</button>
) : (
<button className="button is-info" onClick={() => editingTraining(d.trainingsid)}>Edit</button>
)}
<br />
</div>
))}
)
}
export default Training
The reason you see all fields changing is because when you build the input elements while using .map you are probably assigning the same onChange event and using the same state value to provide the value for the input element.
You should correctly manage this information and isolate the elements from their handlers. There are several ways to efficiently manage this with help of either useReducer or some other paradigm of your choice. I will provide a simple example showing the issue vs no issue with a controlled approach,
This is what I suspect you are doing, and this will show the issue. AS you can see, here I use the val to set the value of <input/> and that happens repeatedly for both the items for which we are building the elements,
const dataSource = [{id: '1', value: 'val1'}, {id: '2', value: 'val2'}]
export default function App() {
const [val, setVal]= useState('');
const onTextChange = (event) => {
setVal(event.target.value);
}
return (
<div className="App">
{dataSource.map(x => {
return (
<div key={x.id}>
<input type="text" value={val} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
This is how you would go about it.
export default function App() {
const [data, setData]= useState(dataSource);
const onTextChange = (event) => {
const id = String(event.target.dataset.id);
const val = String(event.target.value);
const match = data.find(x => x.id === id);
const updatedItem = {...match, value: val};
if(match && val){
const updatedArrayData = [...data.filter(x => x.id !== id), updatedItem];
const sortedData = updatedArrayData.sort((a, b) => Number(a.id) - Number(b.id));
console.log(sortedData);
setData(sortedData); // sorting to retain order of elements or else they will jump around
}
}
return (
<div className="App">
{data.map(x => {
return (
<div key={x.id}>
<input data-id={x.id} type="text" value={x.value} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
What im doing here is, finding a way to map an element to its own with the help of an identifier. I have used the data-id attribute for it. I use this value again in the callback to identify the match, update it correctly and update the state again so the re render shows correct values.
This is code and I am trying to access the result from the api but I am not able to show it on my page.
The results are visible in console but not in page.
I have tried few things but it hasn't worked for me.
I am stuck on this only for the last few days.
import React, { useState } from 'react'
const SearchArea = () => {
const [input,setInput] = useState("");
const [results,setResults] = useState(null)
const onInputChange = (ev) => {
setInput(ev.target.value)
}
const onSearch = () => {
fetch(`https://api.shrtco.de/v2/shorten?url=${input}`)
.then(r=>r.json())
.then(result=>{
setResults(result) ;
console.log(result.result.full_short_link)})
}
const onKeyDown= (ev) =>{
if(ev.keyCode === 13){
onSearch();
}
}
const renderResult = () => {
if(results && results.ok === 0){
return <div>No link to convert.</div>
}
if(results && results.length > 0){
return <div>{results.map((item)=><div>{item.results.result.full_short_link}</div>)}</div>
}
return null
}
return (
<div>
<div className="search-bar">
<input type="text" onKeyDown={onKeyDown} onChange={onInputChange} value={input} className="searching" placeholder="Shorten a link here..."></input>
<button className="short-butt" onClick={onSearch}>Shorten It!</button>
<div>{renderResult()}</div>
</div>
</div>
)
}
export default SearchArea
From what I can see, it seems that you are trying to show MULTIPLE results from the API. So you must start with an array instead of null in the state.
const [results, setResults] = useState([]);
Then for every response from the API, you could either push into the results with results.push() (not recommended) or you could do spread operator like below (more recommended):
fetch(`https://api.shrtco.de/v2/shorten?url=${input}`)
.then((r) => {
return r.json();
})
.then((result) => {
console.log(result.result.full_short_link);
setResults([...results, result.result.full_short_link]); //spread operator
})
.catch((e) => {
console.error(e);
});
Later you can use map on showing the results.
if(results && results.length > 0){
return <div>{results.map((item)=><div>{item.results.result.full_short_link}</div>)}</div>
}
Result:
You can see the code in action: https://codesandbox.io/s/react-link-shorter-7ro9o?file=/index.js
I am having a slight issue with my react code. I want the data to be completely fetched before rendering. However, I have tried various ways such as 'setGroupLoaded to true' after the async call, but it is still not working. When the component first mounts, 'groupLoaded == false and myGroup == [],', then it goes to the next conditional statement where 'groupLoaded == true' but the myGroup [] is still empty. I was expecting the myGroup [] to be filled with data since groupLoaded is true. Please I need help with it.
function CreateGroup({ currentUser }) {
const [name, setName] = useState("");
const [myGroup, setMyGroup] = useState([]);
const [groupAdded, setGroupAdded] = useState(false);
const [groupLoaded, setGroupLoaded] = useState(false);
const handleChange = (e) => {
const { value, name } = e.target;
setName({
[name]: value,
});
};
useEffect(() => {
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
fetchGroupCreated();
setGroupLoaded(true);
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
useEffect(() => {
let groupId = myGroup.length ? myGroup[0].id : "";
let groupName = myGroup.length ? myGroup[0].groupName : "";
if (myGroup.length) {
JoinGroup(currentUser, groupId, groupName);
setTimeout(() => fetchGroupMembers(), 3000);
}
}, [myGroup]);
let itemsToRender;
if (groupLoaded && myGroup.length) {
itemsToRender = myGroup.map((item) => {
return <Group key={item.id} item={item} deleteGroup={deleteGroup} />;
});
} else if (groupLoaded && myGroup.length === 0) {
itemsToRender = (
<div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="exampleInputTitle">Group Name</label>
<input
type="text"
className="form-control"
name="name"
id="name"
aria-describedby="TitleHelp"
onChange={handleChange}
/>
</div>
<button type="submit" className="btn btn-primary">
Add group{" "}
</button>
</form>
</div>
);
}
return (
<div>
<h3>{currentUser ? currentUser.displayName : ""}</h3>
{itemsToRender}
</div>
);
}
The problem is here:
useEffect(() => {
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
fetchGroupCreated();
setGroupLoaded(true);
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
When you call setGroupLoaded(true), the first call to setMyGroup(groupArr) hasn't happened yet because fetchMyGroup(currentUser) is asynchronous. If you've never done this before, I highly recommend putting in some logging statements, to see the order in which is executes:
useEffect(() => {
const fetchGroupCreated = async () => {
console.log("Got data")
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
console.log("Before starting to load")
fetchGroupCreated();
setGroupLoaded(true);
console.log("After starting to load")
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
The output will be:
Before starting to load
After starting to load
Got data
This is probably not the order you expected, but explains exactly why you get groupLoaded == true, but the myGroup is still empty.
The solution (as Nick commented) is to move the setGroupLoaded(true) into the callback, so that it runs after the data is retrieved:
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
setGroupLoaded(true);
};
fetchGroupCreated();
An alternative approach may be to await the call to fetchGroupCreated(). I'm not sure if it'll work, but if it does it'll be simpler:
await fetchGroupCreated();
Hello I have created a search bar with a multipl filter, it works but the functions are too dependent on each other. The problem here is that the functions are handling multiple cases.
would it be possible to lighten each function by chaining them and how ? I don't really get chaining method.
thanks
import React, { useState, useEffect } from "react";
import Search from "./Search";
import Anime from "./Anime";
import "./App.css";
const KIJAN_API_URL = "https://api.jikan.moe/v3/top/anime/1/upcoming";
const App = () => {
const [animes, setAnimes] = useState([]);
const [sortedAnimes, setSortedAnimes] = useState([]);
const [searchValue, setSearchValue] = useState("")
const [filterByType, setFilterByType] = useState("");
const [filterByYear, setFilterByYear] = useState("");
useEffect(() => {
fetch(KIJAN_API_URL)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error("Something went wrong");
}
})
.then(jsonResponse => {
setAnimes(jsonResponse.top);
})
.catch(error => {
console.log(error);
});
}, []);
useEffect(() => {
const callFilterByType = result => {
if (filterByType === "") {
callFilterByYear(result);
console.log(result);
} else {
result = result.filter(anime => anime.type === filterByType);
callFilterByYear(result);
console.log(result);
}
};
const callFilterByYear = result => {
if (filterByYear === "") {
setSortedAnimes(result);
} else {
const regex = new RegExp(`${filterByYear}`, "gi");
result = result.filter(anime => regex.test(anime.start_date));
setSortedAnimes(result);
console.log(result);
}
};
if (searchValue === "") {
callFilterByType(animes);
} else {
const regex = new RegExp(`${searchValue}`, "gi");
console.log("search : ", searchValue);
const result = animes.filter(anime => regex.test(anime.title));
callFilterByType(result);
console.log(result);
}
}, [searchValue, animes, filterByType, filterByYear]);
return (
<div className="App">
<Search
searchValue={searchValue}
setSearchValue={setSearchValue}
filterByType={filterByType}
setFilterByType={setFilterByType}
filterByYear={filterByYear}
setFilterByYear={setFilterByYear}
/>
{sortedAnimes.length > 0 ? (
sortedAnimes.map((anime, index) => {
return <Anime key={index} anime={anime} />;
})
) : (
<span>Aucune correspondance</span>
)}
</div>
);
};
export default App;
SandBox Sample
You can do first round of simplification like this:
useEffect(() => {
let result = [...animes];
if(searchValue) {
const searchRegex = new RegExp(`${searchValue}`, "gi");
result = result.filter(anime => searchRegex.test(anime.title));
}
if(filterByType) {
result = result.filter(anime => anime.type === filterByType);
}
if(filterByYear) {
const yearRegex = new RegExp(`${filterByYear}`, "gi");
result = result.filter(anime => yearRegex.test(anime.start_date));
}
setSortedAnimes(result);
}, [searchValue, animes, filterByType, filterByYear]);
It can be reduced to more compact form, like:
useEffect(() => {
const searchRegex = searchValue && new RegExp(`${searchValue}`, "gi");
const yearRegex = filterByYear && new RegExp(`${filterByYear}`, "gi");
const result = animes.filter(anime =>
(!searchRegex || searchRegex.test(anime.title)) &&
(!filterByType || anime.type === filterByType)) &&
(!yearRegex || yearRegex.test(anime.start_date))
)
setSortedAnimes(result);
}, [searchValue, animes, filterByType, filterByYear]);
More idiomatic way would be use use momoisation hook. i.e. Remove sortedAnimes as state and
const sortedAnimes = useMemo(() => {
const searchRegex = searchValue && new RegExp(`${searchValue}`, "gi");
const yearRegex = filterByYear && new RegExp(`${filterByYear}`, "gi");
return animes.filter(anime =>
(!searchRegex || searchRegex.test(anime.title)) &&
(!filterByType || anime.type === filterByType)) &&
(!yearRegex || yearRegex.test(anime.start_date))
)
}, [searchValue, animes, filterByType, filterByYear]);
try this
if you are using filter method inside jsx then you try this method.
Let me brief it,
consider userInfo like an object containing fields like name, email, location etc. so, if you want your filter method to provide your search results based on these fields value then you can use something like this in jsx.
{userInfo.filter((user) => (
user.name.toLowerCase().includes(cloneSearchTerm)
||
user.email.toLowerCase().includes(cloneSearchTerm)
||
user.location.toLowerCase().includes(cloneSearchTerm)
)
).map((user, idx) => (
<div key={idx}>
<span>{user.name}</span>
<span>{user.email}</span>
<span>{user.location}</span>
</div>
))}