I have an array of users, and every user has its data.
I'm trying to extract user number 1 (id=1) and then save it in useState. After that, I want to map it and print it as li.
so far I've had no success.
import logo from './logo.svg';
import './App.css';
import './myCss.css';
import { useState ,useEffect } from 'react';
import axios from 'axios'
import Page2 from './Page2'
function Page3() {
const [users,setusers] = useState([])
const [user,setUser] = useState({})
useEffect(() => {
async function fetchData() {
let resp = await axios.get("https://jsonplaceholder.typicode.com/users");
setusers(resp.data);
}
fetchData();
}, []);
// In this function I try to print the information
const getData = () =>
{
let myUser2 = users.filter(x=>x.id==1)
setUser(myUser2)
{
user.map((item,index)=>
{
return <li key = {index}>{item}</li>
})
}
}
return (
<div className = "styles">
<input type = "button" value = "Get Data for comp 1" onClick = {getData}/>
</div>
);
}
export default Page3;
You cannot print data in getData which is an event function. Instead of that, you should add it to JSX.
Besides that, user is a single user and users are a list of users. filter is not fit for your case in finding a single user, I'd suggest that you use find instead. find will return an object, so you don't need to use map for renderings.
//`user` is an empty object, so we need to make sure it has data inside `user` with `Object.keys`
{user && Object.keys(user) > 0 && <li key={user.name}>{user.name}</li>)}
Full possible modification
function Page3() {
const [users,setusers] = useState([])
const [user,setUser] = useState({})
useEffect(() => {
async function fetchData() {
let resp = await axios.get("https://jsonplaceholder.typicode.com/users");
setusers(resp.data);
}
fetchData();
}, []);
const getData = () =>
{
let myUser2 = users.find(x=>x.id==1)
setUser(myUser2)
}
return (
<div className = "styles">
<input type = "button" value = "Get Data for comp 1" onClick = {getData}/>
<ul>
{user && Object.keys(user) > 0 && <li key={user.name}>{user.name}</li>)}
</ul>
</div>
);
}
export default Page3;
First I have a question why would you want to map through a state that contains only one value. I see no point in this, however in case you will store more information inside userstate you may try something like the following:
const getData = () => {
let myUser2 = users.filter(x=>x.id==1)
setUser(myUser2)
}
const printUser = user.map((item, index) => <li key={item + index}>{item}</li>
return (
<div className = "styles">
<input type = "button" value = "Get Data for comp 1" onClick = {getData}/>
<ul>
{printUser && printUser}
</ul>
</div>
);
}
export default Page3;
Related
I need to add sorting to fetched data (ascending/descending).
I get all the data from API endpoint. I map every object in that array to be displayed in separate component card. But once I choose to sort data from Descending name I get a quick change of components were they are sorted from Z to A but it just instantly converts back to initial fetched state (from A to Z).
Could you please tell me where the problem is? I don't know why but it feels like sorted array doesn't get saved in state "data" which I use to map all the cards.
import { useState } from 'react';
import { useEffect } from 'react';
import './styles/main.scss';
import Card from './components/Card/Card';
import { v4 as uuidv4 } from 'uuid';
function App() {
const [data, setData] = useState([]);
const [sortType, setSortType] = useState('default');
useEffect(() => {
fetchData();
sortData();
}, [sortType]);
const fetchData = async () => {
const response = await fetch(
'https://restcountries.com/v2/all?fields=name,region,area'
);
const data = await response.json();
setData(data);
};
function sortData() {
let sortedData;
if (sortType === 'descending') {
sortedData = [...data].sort((a, b) => {
return b.name.localeCompare(a.name);
});
} else if (sortType === 'ascending') {
sortedData = [...data].sort((a, b) => {
return a.name.localeCompare(b.name);
});
} else {
return data;
}
setData(sortedData);
}
return (
<div className='content'>
<header className='content__header'>
<h1>Header placeholder</h1>
</header>
<div className='wrapper'>
<div className='wrapper__sort-buttons'>
<select
defaultValue='default'
onChange={(e) => setSortType(e.target.value)}
>
<option disabled value='default'>
Sort by
</option>
<option value='ascending'>Ascending</option>
<option value='descending'>Descending</option>
</select>
</div>
<ul className='wrapper__list'>
{data.map((country) => {
country.key = uuidv4();
return (
<li key={country.key}>
<Card
name={country.name}
region={country.region}
area={country.area}
/>
</li>
);
})}
</ul>
</div>
</div>
);
}
export default App;
This is what I get just for a quick moment:
And then it just goes back to initial state:
It appears the way you're using useEffect is causing your component to refetch the data each time you change the sort type. This could be causing a race condition due to multiple places updating your data state at different times.
I would move the sorting logic into a useMemo and only fetch the data in useEffect on initial load:
import { useEffect, useMemo, useState } from "react";
import './styles/main.scss';
import Card from './components/Card/Card';
import { v4 as uuidv4 } from "uuid";
function App() {
const [data, setData] = useState([]);
const [sortType, setSortType] = useState("default");
// Move sort logic here...
const sortedData = useMemo(() => {
let result = data;
if (sortType === "descending") {
result = [...data].sort((a, b) => {
return b.name.localeCompare(a.name);
});
} else if (sortType === "ascending") {
result = [...data].sort((a, b) => {
return a.name.localeCompare(b.name);
});
}
return result;
}, [data, sortType]);
// Only fetch data once on component mount...
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const response = await fetch(
"https://restcountries.com/v2/all?fields=name,region,area"
);
const data = await response.json();
setData(data);
};
return (
<div className="content">
<header className="content__header">
<h1>Header placeholder</h1>
</header>
<div className="wrapper">
<div className="wrapper__sort-buttons">
<select
defaultValue="default"
onChange={(e) => setSortType(e.target.value)}
>
<option disabled value="default">
Sort by
</option>
<option value="ascending">Ascending</option>
<option value="descending">Descending</option>
</select>
</div>
<ul className="wrapper__list">
{/* Use sortedData here instead of data... */}
{sortedData.map((country) => {
country.key = uuidv4();
return (
<li key={country.key}>
<Card
name={country.name}
region={country.region}
area={country.area}
/>
</li>
);
})}
</ul>
</div>
</div>
);
}
export default App;
Here's a basic example in a Codesandbox (I commented out your styles/card component): https://codesandbox.io/s/goofy-tdd-8lio9?file=/src/App.js
This might be happening for the reason that set state function is asynchronous in nature and the order in which setData is being called is different than you expect.
So, for the initial call with sortType 'default', you are not noticing any change as you are returning the data as it is. But once you change it to 'descending', setData() from sortData() is called earlier than that from fetchData() so as you have already data in your state, you see a change in data in UI for few moments, but then setData() from the function fetchData is called and replaces your data with the one you got from the API call which is unsorted or in ascending order.
POSSIBLE SOLUTION
DON'T set the state inside fetchData method, rather just set it once inside the sortData method, as you are needing it anyhow.
So your code will look something like this:
// we will call sortData inside fetchData so remove it from here
useEffect(() => {
fetchData();
}, [sortType]);
const fetchData = async () => {
const response = await fetch(
'https://restcountries.com/v2/all?fields=name,region,area'
);
const data = await response.json();
// using API response data as an input to sortData function
sortData(data)
};
// using data from parameter instead of state
function sortData(data) {
let sortedData;
if (sortType === 'descending') {
sortedData = [...data].sort((a, b) => {
return b.name.localeCompare(a.name);
});
} else if (sortType === 'ascending') {
sortedData = [...data].sort((a, b) => {
return a.name.localeCompare(b.name);
});
} else {
return data;
}
setData(sortedData);
}
IMPROVEMENT
Your API call is not depending upon the SORTING ORDER, so you don't need to call the API again and again, just call it once, and then sort the data on the value changed from dropdown.
// call the API on initial load only
useEffect(() => {
fetchData();
}, []);
// and on sortType change you can handle it like this:
useEffect(() => {
sortData(data);
}, [sortType]);
// and using this approach you can use the exact same code for both functions implementation that you posted in your question above.
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'm trying to navigate an array of orders stored in each "User". I am able to query and find ones that have orders but I'm not able to display them. I keep getting an error "Cannot read property 'map' of null". Where am I going wrong?
The image below shows how all the orders are stored in "order"
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { firestore } from "../../../FireBase/FireBase";
const OrdersAdmin = (props) => {
const [order, setOrder] = useState(null);
useEffect(() => {
const fetchOrder = async () => {
const doc = await firestore.collection("Users");
const snapshot = await doc.where("orders", "!=", []).get();
if (snapshot.empty) {
console.log("No matching documents.");
return <h1>No Orders</h1>;
}
var ans = [];
snapshot.forEach((doc) => {
console.log(doc.id, "=>", doc.data().orders);
setOrder(doc.data().orders)
});
};
fetchOrder();
}, [props]);
return (
<div className="adminOrders">
<h1>orders</h1>
{console.log(order)}
{order.map((orderItem) => (
<div className="singleOrder" key={orderItem.id}>
<p>{orderItem}</p>
</div>
))}
</div>
);
};
export default OrdersAdmin;
The issue is that the initial value of order is null. null does not have Array.prototype.map, therefore you get the error. Try updating your render to use conditional rendering to only attempt Array.prototype.map when order is truthy and an Array:
{order && order.length > 0 && order.map((orderItem) => (
<div className="singleOrder" key={orderItem.id}>
<p>{orderItem}</p>
</div>
))}
Otherwise you can use a better default value of an empty array for order which would have Array.prototype.map available to execute:
const [order, setOrder] = useState([]);
Hopefully that helps!
// Edit --
This may help:
Project Hatchways
Link to issue -
Issue
As the codes stands right now, the results from the tags still aren't rendering results.
I have a component App.js that renders some children. One of them is 2 search bars. The second search bar TagSearch is supposed to render results from tag creation. What I'm trying to do is pass data from Student where the tags live, and pass them up to the App component in order to inject them into my Fuse instance in order for them to be searched. I have tried to create a function update in App.js and then pass it down to Student.js in order for the tags to update in the parent when a user searches the tags. For some reason, I'm getting a TypeError that states update is not a function.
I put in console logs to track where the tags appear. The tags appear perfectly fine in Student.js, but when I console log them in App.js, the tags just appear as an empty array which tells me they aren't being properly passed up the component tree from Student.js to App.js.
// App.js
import axios from "axios";
import Fuse from "fuse.js";
import Student from "./components/Student";
import Search from "./components/Search";
import TagSearch from "./components/TagSearch";
import "./App.css";
function App() {
const [loading, setLoading] = useState(true);
const [students, setStudents] = useState([]);
const [query, updateQuery] = useState("");
const [tags, setTags] = useState([]);
const [tagQuery, setTagQuery] = useState("");
console.log("tags from app: ", tags);
const getStudents = async () => {
setLoading(true);
try {
const url = `private url for assignment`;
const response = await axios.get(url);
setStudents(response.data.students);
setLoading(false);
} catch (err) {
console.log("Error: ", err);
}
};
const fuse = new Fuse(students, {
keys: ["firstName", "lastName"],
includeMatches: true,
minMatchCharLength: 2,
});
const tagFuse = new Fuse(tags, {
keys: ["text", "id"],
includesMatches: true,
minMatchCharLength: 2,
});
function handleChange(e) {
updateQuery(e.target.value);
}
function handleTags(e) {
setTagQuery(e.target.value);
}
const results = fuse.search(query);
const studentResults = query ? results.map((s) => s.item) : students;
const tagResults = tagFuse.search(tagQuery);
const taggedResults = tagQuery ? tagResults.map((s) => s.item) : tags;
const update = (t) => {
t = tags; // changed this to make sure t is tags from this component's state
setTags(t);
};
useEffect(() => {
getStudents();
}, []);
if (loading) return "Loading ...";
return (
<div className="App">
<main>
<Search query={query} handleChange={handleChange} />
<TagSearch query={tagQuery} handleTags={handleTags} />
{studentResults &&
studentResults.map((s, key) => <Student key={key} students={s} update={update} />)}
{taggedResults &&
taggedResults.map((s, key) => (
<Student key={key} students={s} update={update} />
))}
</main>
</div>
);
}
export default App;
// Student.js
import Collapsible from "../components/Collapsible";
import findAverage from "../helpers/findAverage";
import Styles from "../styles/StudentStyles";
const KeyCodes = {
comma: 188,
enter: 13,
};
const delimiters = [KeyCodes.comma, KeyCodes.enter];
const Student = ({ students, update }) => {
const [isOpened, setIsOpened] = useState(false);
const [tags, setTags] = useState([]);
const collapse = () => {
setIsOpened(!isOpened);
};
const handleDelete = (i) => {
const deleted = tags.filter((tag, index) => index !== i);
setTags(deleted);
};
const handleAddition = (tag, i) => {
setTags([...tags, tag]);
};
useEffect(() => {
update(tags);
}, []);
return (
<Styles>
<div className="student-container">
<img src={students.pic} alt={students.firstName} />
<div className="student-details">
<h1>
{students.firstName} {students.lastName}
</h1>
<p>Email: {students.email}</p>
<p>Company: {students.company}</p>
<p>Skill: {students.skill}</p>
<p>Average: {findAverage(students.grades)}</p>
<Collapsible
students={students}
delimiters={delimiters}
handleDelete={handleDelete}
handleAddition={handleAddition}
isOpened={isOpened}
tags={tags}
/>
</div>
</div>
<button onClick={collapse}>+</button>
</Styles>
);
};
export default Student;
Ciao, try to call update function every time you update tags in Student. Something like this:
const handleDelete = (i) => {
const deleted = tags.filter((tag, index) => index !== i);
setTags(deleted);
update(deleted);
};
const handleAddition = (tag, i) => {
let result = tags;
result.push(tag);
setTags(result);
update(result);
};
In this way, every time you change tags in Student, you will update App state.
An alternative could be use useEffect deps list. In Student, modify useEffect like this:
useEffect(() => {
update(tags);
}, [tags]);
This means that, every time tags will update, useEffect will be triggered and update function will be called.
I'm having troubles to set up react-infinite-scroller within my React component.
I do not want to fetch data via an API directly in my component with loadMore because I already got it from my IndexedDB.
So I want to use my Array dbScans (array with objects) and want to have infinite scroll with max. 3 items of the array.
I tried to create a loadProducts function to slice and concate my array that I want to render but I am getting overload errors, when I try to call it in the Infinite-Scroll component.
import React, { useEffect, useState } from 'react';
import InfiniteScroll from 'react-infinite-scroller';
export default function ProductHistory() {
const [dbScans, setDbScans] = useState<IProductClient[]>([]);
const [loadedProducts, setLoadedProducts] = useState([]);
useEffect(() => {
(async function getDataFromDB() {
setDbScans(await db.yourScans.toArray());
})();
}, []);
let productHistory = dbScans;
// This function is not working
const loadProducts = (page: number) => {
const perPage = 3;
const moreItems: any = dbScans.slice(
page * perPage - perPage,
page * perPage + 1
);
// Am I doing this right?
setLoadedProducts(loadedProducts.concat(moreItems));
}
return (
<>
<div className="product-history">
{ productHistory.length > 0 ?
<InfiniteScroll
pageStart={0}
loadMore={loadProducts(0)} // This is not working
hasMore={true}
loader={<div className="loader" key={0}>Loading ...</div>}
>
{productHistory.map((product: any) =>
<div className="product-history__item" key={product.id}>
<p>{product.name}
</div>
)}
</InfiniteScroll>
: ''
}
</div>
</>
)
}
You should introduce a state variable called as lastObjectPosition which will have the position of the last object that is being shown by the infinite scroll.
const perPage = 3;
const [lastObjectPosition , setLastObjectPosition ] = useState(0);
And then hasMore attribute should be set like this:
hasMore={lastObjectPosition < dbScans.length}
And finally you should modify loadProducts function like this,
const loadProducts = () => {
setLoadedProducts(currentProducts => {
return [
...currentProducts,
dbScans.slice(lastObjectPosition, lastObjectPosition+perPage)
]
});
setLastObjectPosition(currentValue => {
return currentValue + perPage
}
}