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.
Related
I want to use useEffect(on mount) to fetch from API and store it in useState. Fetch API is used to get the data. The problem is when initial page loading and also when I reload the page, it outputs an error called test.map is not a function. Why this happening and how to avoid this ?
import { useEffect, useState } from 'react';
function App() {
const[test, setTest] = useState({})
useEffect(() => {
testfunc()
}, [])
async function testfunc(){
let api = await fetch('https://jsonplaceholder.typicode.com/users')
let apijson = await api.json()
setTest(apijson)
}
return (
<div className="App">
{
test.map((item) => {
return(
<div>
{item.name}
</div>
)
})
}
</div>
);
}
export default App;
You can't map on an object {}, so you should need to define an array [] for the base state :
const[test, setTest] = useState([])
You have to change {} to array first to be able to map over it. You can easily place ? after test like this. or make in the default value of the state a default value for item name. because this error results as you map over an empty object.
import { useEffect, useState } from 'react';
function App() {
const[test, setTest] = useState([{name:"default"}])
useEffect(() => {
testfunc()
}, [])
async function testfunc(){
let api = await fetch('https://jsonplaceholder.typicode.com/users')
let apijson = await api.json()
setTest(apijson)
}
return (
<div className="App">
{
test?.map((item) => {
return(
<div>
{item.name}
</div>
)
})
}
</div>
);
}
export default App;
As already mentioned, you can't use the .map for objects.
Instead of this, you can make something like that
Object.keys(test).map(key => {
const currentSmth = test[key]
return(
<div>
{currentSmth.name}
</div>
)
})
})
I think it helps you to solve your problem.
Be careful using the correct data structures and methods.
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;
This is the structure of the json being fetched. I am trying to render some of the nested threads data to a web page with react.
import react, {useState, useEffect} from "react";
import axios from 'axios'
import ReactJson from 'react-json-view'
const FeaturedBoards = () => {
const [boards, setBoards] = useState([{page: '', threads: {}}]);
useEffect(() => {
fetchBoards();
}, []);
const fetchBoards = () => {
axios.get('https://a.4cdn.org/po/catalog.json')
.then((res) => {
console.log(res.data);
setBoards(res.data);
})
.catch((err) => {
console.log(err);
});
};
if(boards === 0) {
return <div>Loading...</div>;
}
else{
return (
<div>
<h1>Featured Boards</h1>
<div className='item-container'>
{boards.map((board) => (
<div className='board' key={board.id}>
<p>{board['threads']}</p>
</div>
))}
</div>
</div>
);
}
};
export default FeaturedBoards;
I have tried everything to display some of the nested threads data but nothing comes up. I've tried doing a second call to map on board but no luck, storing it in a variable and calling from that still nothing. Am I doing something totally wrong?
I believe this is more fully answered by How can I access and process nested objects, arrays or JSON?. but to explain for this particular data structure, keep reading.
Look at your actual data... boards is an array. Each element in it is an object with page (int) and threads (array) properties. Each threads array element is an object with other properties. You can use map to iterate arrays and return a JSX representation of the objects within.
For example
const [boards, setBoards] = useState([]); // start with an empty array
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchBoards().then(() => setLoading(false))
}, []);
const fetchBoards = async () => {
const { data } = await axios.get('https://a.4cdn.org/po/catalog.json')
setBoards(data)
}
return loading ? <div>Loading...</div> : (
<div>
<h1>Featured Boards</h1>
<div className="item-container">
{boards.map(board => (
<div className="board" key={board.page}> <!-- 👈 note "page", not "id" -->
{board.threads.map(thread => (
<p>{thread.name}</p>
<p>{thread.sub}</p>
<p>{thread.com}</p>
<!-- etc -->
))}
</div>
))}
</div>
</div>
)
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!
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>: