My article state is not returning the proper data? - javascript

I'm still getting the hang of react and react hooks. I'm working on this react challenge found here: Link. My article state is not showing the proper data when the code first load. If you look in the console log my article state shows an empty array then the proper data but I need the proper data when my code first render otherwise my code won't execute right. It says I can't map over undefined. What's wrong with my code?
import React, {useState, useEffect} from 'react';
const Articles = () => {
const [article, setArticle] = useState([])
const [page, setPage] = useState(1)
useEffect(() => {
fetch(`https://jsonmock.hackerrank.com/api/articles?page=${page}`)
.then(res => res.json())
.then(data => setArticle(data))
}, [page])
const pgNums = Array(article.total_pages).fill().map((_, i) => i + 1)
console.log(article)
return (
<React.Fragment>
<div className="pagination">
{pgNums.map(n => <button data-testid="page-button" key={n} onClick={() => setPage(n)}>{n}</button>)}
</div>
<ul className="results">
{/* {article.data.map(article => <li key="title-1" data-testid="result-row">{article.title}</li>)} */}
</ul>
</React.Fragment>
);
}
export default Articles;

The issue is this when you try to access article it is not available. Because of asynchronous api call. What you can do either define another state loading which shows loader when data is fetching or render nothing if data is not available.
In your case you can do like this:
if(!article) return null; <- It means data is not available do not show anything
return (
<React.Fragment>
<div className="pagination">
{pgNums.map(n => <button data-testid="page-button" key={n} onClick={() => setPage(n)}>{n}</button>)}
</div>
<ul className="results">
{/* {article.data.map(article => <li key="title-1" data-testid="result-row">{article.title}</li>)} */}
</ul>
</React.Fragment>

each time page changes you are calling API, you should go with this conditional rendering. Instead of null you should return loading component/waitIndicator etc
const Articles = () => {
const [article, setArticle] = useState([])
const [page, setPage] = useState(1)
const [pgNums, setPageNums] = useState([])
useEffect(() => {
fetch(`https://jsonmock.hackerrank.com/api/articles?page=${page}`)
.then(res => res.json())
.then(data => {
console.log(data.data);
setArticle(data.data)
setPageNums(Array(data.total_pages).fill().map((_, i) => i + 1))
}
)
}, [page])
return (
<React.Fragment>
<div className="pagination">
{pgNums.length > 0 ? pgNums.map(n => <button data-testid="page-button" key={n} onClick={() => setPage(n)}>{n}</button>) : null}
</div>
<ul className="results">
{article.length > 0 ? article.map((article,i) => <li key={i} data-tes.tid="result-row">{article.title}</li>) : null}
</ul>
</React.Fragment>
);
}

Related

Limiting the displayed results in React useEffect

I have some React code that is filtering my array and displaying the results by what the user searches:
function App() {
const [countries, setCountries] = useState([]);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState("");
const [filteredCountries, setFilteredCountries] = useState([]);
useEffect(() => {
setLoading(true);
axios
.get("https://www.countriesapi.com")
.then((res) => {
setCountries(res.data);
setLoading(false);
})
.catch((err) => {
console.log(err);
});
}, []);
useEffect(() => {
setFilteredCountries(
countries.filter((country) =>
country.name.toLowerCase().includes(search.toLowerCase())
)
);
}, [search, countries]);
if (loading) {
return <p>Loading countries...</p>;
}
return (
<div className="App">
<h1>Countries Lists</h1>
<a
style={{ color: "red" }}
target="_blank"
href="https://www.somesite.com"
>
</a>
<input
type="text"
placeholder="Search Countries"
onChange={(e) => setSearch(e.target.value)}
/>
{filteredCountries.map((country, idx) => (
<CountryDetail key={idx} {...country} />
))}
</div>
);
}
My countries list has 500 results so having them all in the page before the user writes the whole search term is not feasible.
How can I display let's say only 10 results in the page, rather than the whole 500?
Thanks
Note: My code has been mirrored by this one if you want to take a look at a live example
You could use Array.prototype.slice() to get only 10 elements.
{filteredCountries.slice(0, 10).map((country, idx) => (
<CountryDetail key={idx} {...country} />
))}
You can paginate your list on axios.get("https://www.countriesapi.com") if www.countriesapi.com allows this. Or you can just make countries.slice
useEffect(() => {
const newList = search ? countries : countries.slice(0, 10)
setFilteredCountries(
newList.filter((country) =>
country.name.toLowerCase().includes(search.toLowerCase())
)
);
}, [search, countries]);

Why will my fetch API call map one nested objects, but not the other?

I'm parsing data from the NASA API using React, and for some reason I can map one nested object within the return but not the other.
Here is my parent component:
import React, { useState } from 'react'
import './NasaAPI.scss'
import NasaImages from './NasaImages'
const NasaAPI = () => {
const [nasaData, setNasaData] = useState([])
const [nasaImage, setNasaImage] = useState("")
const [searchInput, setSearchInput] = useState("")
const [loading, setLoading] = useState(true)
const fetchData = async (e) => {
const data = await fetch(`https://images-api.nasa.gov/search?q=${searchInput}`)
.then(response => response.json())
.then(data => setNasaData(data.collection.items))
.catch(err => console.log(err))
.finally(setLoading(false))
}
const handleSubmit = (e) => {
e.preventDefault()
fetchData()
}
const handleChange = (e) => {
setSearchInput(e.target.value)
}
return (
<div>
<h2>Search NASA Images</h2>
<form onSubmit={handleSubmit}>
<input name="searchValue" type="text" value={searchInput} onChange={handleChange}></input>
<button value="Submit">Submit</button>
</form>
<section>
<NasaImages nasaData={nasaData} loading={loading}/>
</section>
</div>
)
}
export default NasaAPI
Here's where the issue is, in the child component:
import React from 'react'
const NasaImages = ({ nasaData }) => {
console.log(nasaData)
return (
<div>
<h2>This is a where the data go. 👇</h2>
{
nasaData && nasaData.map((data, idx) => {
return (
<div key={idx}>
<p>{data.href}</p>
<div>
{/* {data.links.map((data) => {
return <p>{data.href}</p>
})} */}
{data.data.map((data) => {
return <p>{data.description}</p>
})}
</div>
</div>
)
})
}
</div>
)
}
export default NasaImages
The current configuration works, and will display a data.description (data.data.map) mapping property. However, I want the commented code immediately above it to work which displays a data.href (data.links.map) property.
The JSON looks as follows:
So, the issue is that I can map one set of properties, data.data.map, but cannot access the other in the same object, data.links.map, without getting the error "TypeError: Cannot read property 'map' of undefined". Thank you in advance!
There exists a data element sans a links property, in other words there is some undefined data.links property and you can't map that. Use Optional Chaining operator on data.links when mapping, i.e. data.links?.map. Use this on any potentially undefined nested properties.
const NasaImages = ({ nasaData = [] }) => {
return (
<div>
<h2>This is a where the data go. 👇</h2>
{nasaData.map((data, idx) => (
<div key={idx}>
<p>{data.href}</p>
<div>
{data.links?.map((data, i) => <p key={i}>{data.href}</p>)}
{data.data?.map((data, i) => <p key={i}>{data.description}</p>)}
</div>
</div>
))}
</div>
)
}

Why is the fetch statement in my react app resulting in two calls?

Can anybody please explain to me why the fetch statement is resulting in 2 API calls? Both the chrome console and dev tools > network tab is showing two versions. The following is the code that I am using.
import React, { useState } from 'react';
import './contact.css';
const App = () => {
const [contacts, setContacts] = useState([]);
fetch("https://randomuser.me/api/?results=3")
.then(response => response.json())
.then(data => console.log(data));
return (
<>
{
contacts.map(contact => (
<ContactCard
avatar="https://via.placeholder.com/150"
name={contact.name}
email={contact.email}
age={contact.age}
/>
))
}
</>
)
};
const ContactCard = props => {
const [showAge, setShowAge] = useState(false);
return (
<div className="contact-card">
<img src="https://via.placeholder.com/150" alt="profile" />
<div className="user-details">
<p>Name: {props.name}</p>
<p>Email: {props.email}</p>
<button onClick={() => setShowAge(!showAge)}>{!showAge ? 'Show' : 'Hide'} Age</button>
{
showAge && <p>Age: {props.age}</p>
}
</div>
</div>
);
};
export default App;
const App = () => {
const [contacts, setContacts] = useState([]);
// the issue is here, each time the component renders this statement will be exectuted
fetch("https://randomuser.me/api/?results=3")
.then(response => response.json())
.then(data => console.log(data));
// if you want to execute code after component is mounted into dom, use useEffect
// like this
useEffect(() => {
fetch("https://randomuser.me/api/?results=3")
.then(response => response.json())
.then(data => console.log(data));
}, []) // the second param for useEffect is dependencies array, pass an empty array if you want your effect to run only once (which is equivalent to componentDidMount in react class based components)
return (
<>
{
contacts.map(contact => (
<ContactCard
avatar="https://via.placeholder.com/150"
name={contact.name}
email={contact.email}
age={contact.age}
/>
))
}
</>
)
};

How can I set a loading state on a specific element of an array?

I have a list of components coming out of an array of objects. See the code:
const SitesList = () => {
const [loadingState, setLoadingState] = useState(false);
const copySite = (site) => {
setLoadingState(true);
copySiteAction(site)
.then(() => setLoadingState(false))
.catch(() => setLoadingState(false));
};
return filterSites.map((s, i) => (
<div>
{!loadingState ? (
<>
<p>Site: {s.name}</p>
<button onClick={() => copySite(s)}>Copy Site</button>
</>
) : (
<LoadingStateComponent />
)}
</div>
));
};
I want to show the component <LoadingStateComponent /> only on the index/component when I click the Copy Site button. As it is right now, every time I click on that button, all of the components disappear, hence what I see is the <LoadingStateComponent /> until loadingState is set to false.
Any ideas on how can I achieve this?
Rather than having a single "loading" state value, you need one for each item in the array. So, you basically need a second array that's the same size as filterSites to track the state of each index. Or, perhaps a better way would be a simple map linking indices to their corresponding loading state, so that you don't even need to worry about creating an array of X size.
const SitesList = () => {
const [loadingState, setLoadingState] = useState({});
const copySite = (site, index) => {
setLoadingState({...loadingState, [index]: true});
copySiteAction(site)
.then(() => setLoadingState(oldLoadingState => ({...oldLoadingState, [index]: false})))
.catch(() => setLoadingState(oldLoadingState => ({...oldLoadingState, [index]: false})));
};
return filterSites.map((s, i) => (
<div>
{!loadingState[i] ? (
<>
<p>Site: {s.name}</p>
<button onClick={() => copySite(s, i)}>Copy Site</button>
</>
) : (
<LoadingStateComponent />
)}
</div>
));
};
This may or may not solve your problem, but it will solve other problems and its too long for a comment.
This:
const SitesList = () => {
const [loadingState, setLoadingState] = useState(false);
const copySite = (site) => {
setLoadingState(true);
copySiteAction(site)
.then(() => setLoadingState(false))
.catch(() => setLoadingState(false));
};
return filterSites.map((s, i) => (
<div>
{!loadingState ? (
<>
<p>Site: {s.name}</p>
<button onClick={() => copySite(s)}>Copy Site</button>
</>
) : (
<LoadingStateComponent />
)}
</div>
));
};
Needs to look more like this:
const SitesList = () => {
const [loadingState, setLoadingState] = useState(false);
useEffect(() => {
if (loadingState) {
apiCall()
.then(whatever)
.catch(whatever)
.finally(() => setLoadingState(false));
}
}, [loadingState]);
return filterSites.map((s, i) => (
<div key={s.id}> // Note the key
{!loadingState ? (
<>
<p>Site: {s.name}</p>
<button onClick={() => setLoadingState(true)}>Copy Site</button>
</>
) : (
<LoadingStateComponent />
)}
</div>
));
};
React assumes that your pure functional component is a pure function. If you violate that assumption it is under no obligation to respect your side-effects or render them when you'd expect. You need to wrap any side-effects (like a xhr request) in a useEffect hook, and like all hooks it must be called unconditionally. The way I've written it the hook will be called every time loadingState changes and will make the call if it is true, you may need to tweak for your actual use case.
Additionally, items rendered from an array need a unique key prop, I'm assuming your sites have ids but you will need to figure out a way to generate one if not.

Pokemon API Uncaught TypeError: Cannot read property '0' of undefined

I'm trying to access the abilities of my pokemon, but I keep getting the same error. I'm using React hooks to build my project, and the data that I fetched from Pokemon API was set to setWildPokemon. If I put wildPokemon.name, I'll get the name of the pokemon, which is fine. This also works when I output wildPokemon.abilities. However, when I start getting deeper into my nested objects, that's when things go
function App() {
const [pokedex, setPokedex] = useState([]);
const [wildPokemon, setWildPokemon] = useState({});
const [storeCard, setStoreCard] = useState({});
const { id, sprites } = wildPokemon;
// console.log(id, sprites.back_shiny);
console.log(wildPokemon);
console.log(wildPokemon.name);
// console.log(wildPokemon.types[0].name);
useEffect(() => {
encounterWildPokemon();
}, []);
const pokeId = () => {
const min = Math.ceil(1);
const max = Math.floor(151);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
const encounterWildPokemon = () => {
axios
.get(`https://pokeapi.co/api/v2/pokemon/${pokeId()}`)
.then(response => {
setWildPokemon(response.data);
});
};
const catchPokemon = pokemon => {
setPokedex(state => {
const monExists = state.filter(p => pokemon.id === p.id).length > 0; // mostly false. Only true if you catch the same pokemon
if (!monExists) {
state = [...state, pokemon];
state.sort(function(a, b) {
return a.id - b.id;
});
}
return state;
});
encounterWildPokemon(); // MISTAKE: we have to call this function whenever we're done
};
const releasePokemon = id => {
setPokedex(state => state.filter(p => p.id != id));
};
// PokeModal
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = pokemon => {
setShow(true);
setStoreCard(pokemon);
};
// JSX
return (
<div className="app-wrapper container">
<header>
<h1 className="title">React Hooks</h1>
{/* <img src="{sprites[0].back_default}" /> */}
<h3 className="subtitle">With Pokémon</h3>
</header>
<section className="wild-pokemon">
<h2>Wild Encounter</h2>
<img
src={
"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/" +
wildPokemon.id +
".png"
}
className="sprite"
alt=""
/>
<h3>{wildPokemon.name}</h3>
<button className="catch-btn" onClick={() => catchPokemon(wildPokemon)}>
CATCH
</button>
</section>
UPDATED:
Ok, I just solved the problem =). I was trying to access the data as soon as the webpage renders. Since fetching is asynchronous, I was basically trying to get the data that hasn't existed yet, which is an empty object.
The state is an empty object on the initial load, and that's what is used for the first render. When you try to access sprites[0], sprites is undefined, since the data hasn't been loaded yet. one way to solve this issue is to delay the render until the data is fetched:
return (
sprites && sprites.length && (
<div className="app-wrapper container">
<header>
<h1 className="title">React Hooks</h1>
<img src={sprites[0].back_default} />
<h3 className="subtitle">With Pokémon</h3>
</header>
...
)
Alternatively you can use a loading state and set it to true until the data is fetched. Helpful when you want to show a loader meanwhile.
const [loading, setLoading] = useState(true);
const encounterWildPokemon = () => {
setLoading(true);
axios
.get(`https://pokeapi.co/api/v2/pokemon/${pokeId()}`)
.then(response => {
setWildPokemon(response.data);
setLoading(false);
});
};
// JSX
return (
loading ? <p>Loading...</p> : (
<div className="app-wrapper container">
<header>
<h1 className="title">React Hooks</h1>
<img src={sprites[0].back_default} />
<h3 className="subtitle">With Pokémon</h3>
</header>
...
)

Categories

Resources