Fetch image based on text and display from API react - reactjs

I've retrieved a list of categories using an API. Now I want to fetch images from an URL based on the categories. I tried using each category to fetch images from another API, but I'm not sure how to do it.
import React, { useEffect, useState } from 'react';
import './css/Category.css';
function Category() {
useEffect(() => {
fetchData();
getImage();
}, []);
const [categories, setCategories] = useState([]);
const [image, setImage] = useState('');
const fetchData = async () => {
const data = await fetch('https://opentdb.com/api_category.php')
const categories = await data.json();
console.log(categories.trivia_categories)
setCategories(categories.trivia_categories)
}
const getImage = async (name) => {
console.log(name)
const q = name.split(' ').join('+')
const img = await fetch(`https://pixabay.com/api/?key=apikey&q=${q}&image_type=photo`)
const image = await img.json();
console.log(image)
setImage(image.previewURL)
}
return (
<div className="categories">
Yesss
<div className="category-grid">
{categories.map(category => (
<div className="category">
{category.name}
<img src={getImage(category.name)} /> //do not know what to do here to fetch image of the respective category
</div>
))}
</div>
</div>
)
}
export default Category;
After changes suggested by Noah, I was able to show only one image.
const getImage = async (name) => {
const query = stringMan(name.name)
console.log(query)
const img = await fetch(`https://pixabay.com/api/?key=17160673-fd37d255ded620179ba954ce0&q=${query}&image_type=photo`)
const image = await img.json();
console.log(image)
setImage({ [name.name]: image.hits[0].largeImageURL })
}
return (
<div className="categories">
Yesss
<div className="category-grid">
{categories.map(category => (
<div className="category" key={category.id}>
{category.name}
<img key={category.id} src={image[category.name]} />
</div>
))}
</div>
</div>
)

There are a couple of changes that you can make here.
One issue that I see is that you have a single image variable, that's being re-used for every single category. So when you map over a list of categories (for example let's say we have categories: [history, science, and math]). The current code will call getImage three times, with history, science, and math as parameters.
However, there is only one state variable that is being written to. Which means the last execution of setImage is the only one that will be preserved.
So, you might want to change image from being the URL of a category image, to an object that has the shape:
{
history: [url],
science: [url],
math: [url]
}
The other change to make is that you are calling the getImage() function directly in the rendered output <img src={getImage(category.name)} />. Instead, this should simply use the value that was assigned to the image state: <img src={image} />.
To actually fetch the image, you can use the useEffect hook (https://reactjs.org/docs/hooks-effect.html) to react to changes to the categories variable. That might look something like:
useEffect(() => {
categories.forEach((c) => getImage(c));
}, [categories]);
The useEffect hook will invoke the function it is given, whenever the dependencies change. This will allow you to trigger the getImage function in response to changes to the categories.

There're lot of improvement that could be done as stated by #noah-callaway above/below but coming straight to the point you need to simply fix the URI creation logic to use encodeURIComponent like below:
import React, { useEffect, useState } from 'react';
function Category() {
useEffect(() => {
fetchData();
getImage();
}, []);
const [categories, setCategories] = useState([]);
const [image, setImage] = useState('');
const fetchData = async () => {
const data = await fetch('https://opentdb.com/api_category.php')
const categories = await data.json();
console.log(categories.trivia_categories)
setCategories(categories.trivia_categories)
}
const getImage = async (name) => {
return encodeURI(`https://pixabay.com/api/?key=apikey&q=${encodeURIComponent(name)}&image_type=photo`)
}
return (
<div className="categories">
Yesss
<div className="category-grid">
{categories.map(category => (
<div className="category">
{category.name}
<img src={getImage(category.name)} />
</div>
))}
</div>
</div>
)
}
don't have the api key so can't test but it'll give you something like
https://pixabay.com/api/?key=apikey&q=Entertainment%3A%20Comics&image_type=photo
good luck, hope it works.

Related

Sort fetched data

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.

Rendering nested json response data in react UI

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>
)

React, unexpected multiple result when using map and fetch

I am building Weather App, my idea is to save city name in localStorage, pass a prop to child component, then iterate using map and display each in seperate child of the first child
The problem is that displayed data doubles/triples on render(depending on component when render occurs) so when I have for example city London and add city Berlin it will render:
London,London,Berlin
The problem is not in AddCity component, it's working correctly but in this mix of asynchronous setState/fetching and maping
Please see the code below
App(parent component)
const App = () => {
const [cities, setCities] = useState([]);
const addCity = (newCity)=>{
console.log('adding')
setCities([...cities, newCity]);
let cityId = localStorage.length;
localStorage.setItem(`city${cityId}`, newCity);
}
useEffect(() => {
loadCityFromLocalStore()
}, [])
const loadCityFromLocalStore =()=>{
setCities([...cities, ...Object.values(localStorage)])
}
return (
<div>
<Header />
<AddCity addCity={addCity}/>
<DisplayWeather displayWeather={cities}/>
</div>
)
}
DisplayWeather (first child)
const DisplayWeather = ({displayWeather}) => {
const apiKey = '4c97ef52cb86a6fa1cff027ac4a37671';
const [fetchData, setFetchData] = useState([]);
useEffect(() => {
displayWeather.map(async city=>{
const res =await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData((fetchData=>[...fetchData , data]));
})
}, [displayWeather])
return (
<>
{fetchData.map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
}
Weather component
const Weather = ({data}) => {
return (
<li>
{data.name}
</li>
)
}
It looks like the problem comes from calling setFetchData for cities that you already added previously.
One easy way to fix it would be to store fetch data as an object instead of a dictionary so that you just override the data for the city in case it already exists (or maybe even skip the fetch as you already have the data).
For example:
const [fetchData, setFetchData] = useState({});
useEffect(() => {
displayWeather.map(async city=>{
const res = await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData((fetchData=> ({...fetchData, [city]: data})));
})
}, [displayWeather])
Then, to map over fetch data you can just use Object.values:
return (
<>
{Object.values(fetchData).map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
If you want to skip already fetched cities you can do something like this instead:
useEffect(() => {
displayWeather.map(async city=>{
if (!fetchData[city]) {
const res = await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData((fetchData=> ({...fetchData, [city]: data})));
}
})

useState creates duplicates - infinite scrolling - Warning: Encountered two children with the same key

To give some insight what I am trying to archive:
So basically the second useEffect renders a navlist, clicking on a navlist-item should render the correct genre movie-list, it worked until, I tried to implement infinite scrolling... now after clicking the navlist-item, it always adds the movies from the nav-item before, which creates duplicates.
And I get this Errors:
Warning: Encountered two children with the same key. Also React tells me to add movies as dependency in the first useEffect. But if I do so, it triggers a infinite loop.
I think the core problem is: const newMovieList = [...movies, ...data.results] and the missing dependency.
Probably another problem is adding these many dependencies to a useEffect in the first place?
I tried for hours to fix it, but there always some weird side effects.. like one duplicate movie or some genre are working and others not.
Any advice or help, how to fix it, would be great.
import React, { useEffect, useState } from "react";
import { NavLink, useParams, useRouteMatch } from "react-router-dom";
import Movies from "./Movies";
export default function TestMovie(props) {
const [loading, setLoading] = useState(false);
const [genres, setGenres] = useState([]);
const [movies, setMovies] = useState([]);
const [pageNumber, setPageNumber] = useState(1);
const params = useParams();
const paramsId = params.id;
const route = useRouteMatch();
useEffect(() => {
(async () => {
setLoading(true);
try {
let response = "";
if (route.path === "/") {
response = await fetch(
`https://api.themoviedb.org/3/movie/upcoming?api_key=${apiKey}&page=${pageNumber}`
);
} else if (paramsId === "23") {
response = await fetch(
`https://api.themoviedb.org/3/movie/popular?api_key=${apiKey}&page=${pageNumber}`
);
} else {
response = await fetch(
`https://api.themoviedb.org/3/discover/movie?api_key=${apiKey}&with_genres=${paramsId}&page=${pageNumber}`
);
}
const data = await response.json();
const newMovieList = [...movies, ...data.results];
setMovies(newMovieList);
console.log("PageNumber: " + pageNumber);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
})();
}, [setMovies, paramsId, route.path, pageNumber]);
useEffect(() => {
(async () => {
try {
const response = await fetch(
`https://api.themoviedb.org/3/genre/movie/list?api_key=${apiKey}`
);
const data = await response.json();
const newGenres = [{ id: 23, name: "Popular" }, ...data.genres];
setGenres(newGenres);
} catch (error) {
console.log(error);
}
})();
}, [setGenres]);
return (
<>
<ul className="genre-list">
<li className="genre-list__item">
<NavLink exact activeClassName="active" to="/">
Upcoming
</NavLink>
</li>
{genres.map((genre) => (
<li className="genre-list__item" key={genre.id}>
<NavLink
exact
activeClassName="active"
to={`/genre/${genre.id}-${genre.name}`}
>
{genre.name}
</NavLink>
</li>
))}
</ul>
<Movies
setPageNumber={setPageNumber}
movies={movies}
loading={loading}
></Movies>
</>
);
}
The InfiniteScroll part:
export default function Movies(props) {
const { movies, loading, setPageNumber } = props;
function updatePageNumber() {
setPageNumber((pageNumber) => pageNumber + 1);
}
return loading ? (
<div className="loader">
<Loader />
</div>
) : (
<>
<InfiniteScroll
dataLength={movies.length}
next={updatePageNumber}
hasMore={true}
>
<div className="movies-layout">
{movies.map((movie) => (
<Movie key={movie.id} movie={movie}></Movie>
))}
</div>
</InfiniteScroll>
</>
);
}
There might be different ways to do infinite scroll. Based on your way, the main issue is this line
const newMovieList = [...movies, ...data.results];
You can't be sure the new result should be the new one, because user could hit old page (or change "rows per page" settings).
In order to avoid that, maybe the easiest way for you is to make a function to add item by item key.
function getNewMovieList(current, data) {
const movies = {}
[...current, ...data].forEach(item => {
movies[item.id] = item
})
return Object.values(movies)
}
Literally you want to make sure it's still a new list without duplicated items. The error you are getting actually serve you good in this case.
Another way
Another way is to put fetch call inside a page component,
const Page = ({ pageId }) => {
const [data, setData] = useState([])
useEffect(() => { go fetch data for this page ONLY })
}
Now if you go to <Page pageId={5} />, you won't run into the data duplication issue at all.
But it probably depends on how you expect from the user, if you want to have smooth scroll behavior, maybe your approach is still the way to go, but if you can afford user to click any page, then second approach might be much safer and scalable.

React add comma one single string

I am using react app. I fetched one data from open api. in that api the ingredient in one single string but divided by \n1. When I fetched the data it came like this one single string and the \n1 shows like (, divided). I wanted to put the ingredients in the ul li elements or and comma-a after each ingredient. I tried lots of ways to split the data and also tried to add a comma after each word but it did not work. I shared my code in codesandbox.
This is my code
import React, { useEffect } from "react";
import "./styles.css";
export default function App() {
const [state, setState] = React.useState([]);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const response = await fetch("https://sampleapis.com/recipes/api/recipes");
const data = await response.json();
setState(data);
};
return (
<div className="App">
{state.map((recipe) => {
return (
<>
<div key={recipe.id}>
<h1>{recipe.title}</h1>
<p>{recipe.ingredients}</p>
</div>
</>
);
})}
</div>
);
}
try to split and mapping them in separate html elements
<div className="App">
{state.map((recipe) => {
return (
<div key={recipe.id}>
<h1>{recipe.title}</h1>
<div>{recipe.ingredients?.split("\n").map(ingre=><p>{ingre}</p>)}</div>
</div>
);
})}
</div>
I found solution and did it like this:
const ingre = newRecipe.ingredients // this is the data
var result = ingre.split(',')
.map(word => `${word.trim()}`)
.join(', ');

Resources