I am trying to fetch images by their ids. The architecture of backend is as follows: DB stores images in binary and there is another table that stores images ids.
I am using apollo client on front end to prefetch images ids and then send another set of fetch requests.
Unfortunately I get Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. Could anyone help me to
1) figure out why it happens. I see that there is bunch of pending promises in the stack.
and 2) how it can be refactored to better architecture.
import React, {useState} from 'react'
import {useQuery} from "#apollo/react-hooks";
import {gql} from 'apollo-boost';
const apiEndpoint = 'http://localhost:9211';
const getProductImage = function (id) {
return gql`
{
productById(id: "${id}") {
images {
imageId
}
}
}`
};
const fetchImage = (imageUrl, allImgsArr) => {
return fetch(imageUrl)
.then(res => res.blob())
.then(img => allImgsArr.push(URL.createObjectURL(img)))
};
const ItemPage = (props) => {
const [id] = useState(props.match.params.id);
const {data} = useQuery(getProductImage(id));
let imagesIds = [];
if (data) {
data.productById.images.forEach(image => {
imagesIds.push(image.imageId)
});
}
const [imagesUrls, setImagesUrl] = useState([]);
// MULTIPE FETCH RETRIEVALS START
for (let imId of imagesIds) {
setImagesUrl(imagesUrls => [...imagesUrls, fetchImage(`${apiEndpoint}/image/${imId}`, imagesUrls)]);
}
// MULTIPE FETCH RETRIEVALS END
return (
<>
<div>
<div>
<img src={imagesUrls[0] ? imagesUrls[0] : ''} alt="main item 1 photo"/>
</div>
<div>
<div>
<img src={imagesUrls[1] ? imagesUrls[1] : ''} alt="Additional item 1 photo"/>
</div>
</div>
</div>
</>
)
};
export default ItemPage;
your query should be a constant , not function.
const GET_PRODUCT_IMAGE = gql`
query getProduct($id:String!) {
productById(id: $id) {
images {
imageId
}
}
}
}`
// pass variables like this
const {data} = useQuery(GET_PRODUCT_IMAGE, { variables: { id },
});
More Info : https://www.apollographql.com/docs/react/data/queries/
Related
I am developing a MERN stack app where I am trying to implement server side pagination.
Note that I am using React Query for managing server state.
On the root route, I display two posts.
When I click on the Next button, NEXT two blog posts should be displayed. However, this is not happening. I see the same two posts on page 2 as on page 1.
Where is the problem?
I think, there is either some problem with my server side pagination logic or with React Query. I suspect React Query is not fetching blog posts when I click on the Next button; instead it is fetching the posts from the cache. (I could be wrong here).
These are my code snippets:
postControllers.js
const asyncHandler = require("express-async-handler");
const Post = require("../models/postModel");
const fetchAllPosts = asyncHandler(async (req, res) => {
const { pageNumber } = req.body;
const pageSize = 2;
const postCount = await Post.countDocuments({});
const pageCount = Math.ceil(postCount / pageSize);
const posts = await Post.find({})
.limit(2)
.skip(pageSize * (pageNumber - 1));
res.json({ posts, pageCount });
});
postRoutes.js
const express = require("express");
const { fetchAllPosts, createPost } = require("../controllers/postControllers");
const router = express.Router();
router.get("/api/posts", fetchAllPosts);
module.exports = router;
Posts.js
import React, { useState } from "react";
import { useFetchAllPosts } from "../hooks/postsHooks";
import Spinner from "../sharedUi/Spinner";
const Posts = () => {
const [pageNumber, setPageNumber] = useState(1);
const { data, error, isLoading, isError } = useFetchAllPosts(pageNumber);
const handlePrevious = () => {
setPageNumber((prevPageNum) => prevPageNum - 1);
};
const handleNext = () => {
setPageNumber((prevPageNum) => prevPageNum + 1);
};
return (
<div>
{isLoading ? (
<Spinner />
) : isError ? (
<p>{error.message}</p>
) : (
data.posts.map((post) => (
<p className="m-6" key={post.title}>
{post.title}
</p>
))
)}
<div>
{isLoading ? (
<Spinner />
) : isError ? (
<p>{error.message}</p>
) : (
<div className="flex justify-between m-6 w-60">
<button
disabled={pageNumber === 1}
className="bg-blue-400 px-1 py-0.5 text-white rounded"
onClick={handlePrevious}
>
Previous
</button>
<p>{data.pageCount && `Page ${pageNumber} of ${data.pageCount}`}</p>
<button
disabled={pageNumber === data.pageCount}
className="bg-blue-400 px-1 py-0.5 text-white rounded"
onClick={handleNext}
>
Next
</button>
</div>
)}
</div>
</div>
);
};
export default Posts;
postHooks.js
import { useQuery, useMutation, useQueryClient } from "#tanstack/react-query";
import { useNavigate } from "react-router-dom";
import axios from "axios";
export const useFetchAllPosts = (pageNumber) => {
const response = useQuery(["posts"], async () => {
const { data } = await axios.get("/api/posts", pageNumber);
return data;
});
return response;
};
all dependencies of a query (= everything that is used inside the query function) needs to be part of the query key. React Query will only trigger auto fetches when the key changes:
export const useFetchAllPosts = (pageNumber) => {
const response = useQuery(["posts", pageNumber], async () => {
const { data } = await axios.get("/api/posts", pageNumber);
return data;
});
return response;
};
to avoid hard loading states between pagination, you might want to set keepPreviousData: true
see also:
the official pagination example
the guide on paginated queries
I'm trying to display an array with 151 pokemons sorted by their pokedex positions (ex: 1 - bulbasaur, 2- ivysaur...), but every time I reload the page it brings me a different array, with the positions shuffled.
App.js:
import './App.css';
import { useEffect, useState } from 'react';
import Pokemon from './components/Pokemon';
const App = () => {
const [pokemonData, setPokemonData] = useState([]);
const fetchPokemons = () => {
for (let index = 1; index < 152; index += 1) {
new Array(151).fill(
fetch(`https://pokeapi.co/api/v2/pokemon/${index}`)
.then((data) => data.json())
.then((result) =>
setPokemonData((prevState) => [...prevState, result])
)
);
}
};
useEffect(() => {
fetchPokemons();
}, []);
return (
<>
{pokemonData && <Pokemon data={pokemonData} />}
</>
);
};
export default App;
Pokemon.js:
const Pokemon = ({ data }) => {
return (
<section>
{data.map((pokemon) => (
<div key={pokemon.name}>
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
<span>{pokemon.name}</span>
<span>
{pokemon.types.length >= 2
? `${pokemon.types[0].type.name} ${pokemon.types[1].type.name}`
: pokemon.types[0].type.name}
</span>
</div>
))}
</section>
);
};
export default Pokemon;
If you want to guarantee the order, you'll need something like
const fetchPokemons = async () => {
const promises = [];
for (let index = 1; index < 152; index += 1) {
promises.push(fetch(`https://pokeapi.co/api/v2/pokemon/${index}`).then((data) => data.json()));
}
const results = await Promise.all(promises);
setPokemonData(results);
};
This will take a while as it loads all of the pokémon before showing any of them - if you don't want that, then there really are two options: rework things to do each request sequentially, or alternately switch to an array where some of the slots may be null while things are still being loaded (which will require changing your rendering code some too).
You are making 153 calls to api, which is not great I would highly recommend that you change into single api call to get all pokemons, to achieve this you can do it like this:
const [pokemonData, setPokemonData] = useState<any>([]);
const fetchPokemons = async () => {
const data = await fetch(`https://pokeapi.co/api/v2/pokemon?offset=0&limit=153"`);
const pokemons = await data.json();
setPokemonData(pokemons.results);
};
useEffect(() => {
(async () => {
await fetchPokemons();
})();
}, []);
Also this will guarantee that you get data always in the same way. You will not face any race conditions and you won't any unnecessary api calls.
I am new to Apollo Client and want to implement pagination. My code looks like this:
I am using RickandMorty endpoint for this (https://rickandmortyapi.com/graphql)
useCharacters.tsx
import { useQuery, gql } from '#apollo/client';
const GET_ALL_CHARACTERS = gql`
query GetCharacters($page: Int) {
characters(page: $page) {
info {
count
pages
}
results {
id
name
}
}
}
`;
export const useCharacters = (page: number = 1) => {
const { data, loading, error } = useQuery(GET_ALL_CHARACTERS, { variables: { page } });
return { data, loading, error };
};
App.tsx
export const App = () => {
const { data, loading, error } = useCharacters(1);
const nextPage = () => {
const { data, loading, error } = useCharacters(2);
};
return (
<>
{loading ? (
<div> Loading... </div>
) : error ? (
<div>Error</div>
) : (
<>
<CharacterList data={data.characters.results} />
<div onClick={nextPage}> Next </div>
</>
);
};
It is fetching data properly the first time but I want to fetch new data when Next button is clicked on page 2.
I know I can't call useQuery() in a method like this as hooks cannot be called inside a block and also the data, error, and loading won't be accessible outside.
How can I fix this issue? I tried googling it but could not find any help related to this.
This might help other developers who are new to Apollo Client and will save them time.
fetchMore() can be used for pagination with Apollo Client.
useCharacters.tsx
export const useCharacters = (page: number = 1, name: string = '') => {
const { data, loading, error, fetchMore } = useQuery(GET_ALL_CHARACTERS, {
variables: { page, name },
notifyOnNetworkStatusChange: true, // to show loader
});
return { data, loading, error, fetchMore }; // returning fetchMore
};
App.tsx
export const App = () => {
const { data, loading, error, fetchMore } = useCharacters(1);
const nextPage = () => {
/* You can call the returned fetchMore() here and pass the next page number.
updateQuery() simply updates your data to the newly fetched records otherwise return previous records
*/
fetchMore({
variables: {
page: 2,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return fetchMoreResult;
},
});
};
return (
<>
{loading ? (
<div> Loading... </div>
) : error ? (
<div>Error</div>
) : (
<>
<CharacterList data={data.characters.results} />
<div onClick={nextPage}> Next </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 have a component that uses axios to access the PubMed api (in componentDidMount), retrieves some publication ids then stores them in state as "idlist". A second function is then called (addPapers) which passes in this id list and makes a second api call to retrieve further details (title, journal, authors) for each id. All this seems to work fine and when I use react tools to check state there is an array ("paperList") full of objects that have the expected key:value pairs. However, when I try to map over this array and access the values within the objects in the render function (ie paper.title, paper.author, paper.journal) they are returning as undefined. I haven't been using react for long and suspect I am making a basic mistake but cant figure it out.
I have tried console.logging each step and the expected data is in state and correct in react tools
import axios from 'axios'
import './App.css';
import rateLimit from 'axios-rate-limit';
class App extends Component {
state= {
idlist: [],
papersList : ""
}
componentDidMount () {
console.log("incomponent")
axios.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&retmode=json&retmax=1000&term=((Australia%5Bad%5D)%20AND%20(%222019%2F07%2F01%22%5BDate%20-%20Publication%5D%20%3A%20%223000%22%5BDate%20-%20Publication%5D))%20AND%20(%22nature%22%5BJournal%5D%20OR%20%22Nature%20cell%20biology%22%5BJournal%5D%20OR%20%22Nature%20structural%20%26%20molecular%20biology%22%5BJournal%5D)")
.then (response =>
this.setState({idlist: response.data.esearchresult.idlist}, () => {
this.addPapers(this.state.idlist)
}
)
)}
addPapers = (idlist) => {
if (idlist) {
const http = rateLimit(axios.create(), { maxRequests: 6, perMilliseconds: 1000 })
const list = this.state.idlist.map(id => {
let paperObj ={};
let paperList =[]
http.get(`https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&rettype=abstract&id=${id}&api_key=9476810b14695bd14f228e63433facbf9c08`)
.then (response2 => {
const title = response2.data.result[id].title
const journal = response2.data.result[id].fulljournalname
const authorList = []
const authors = response2.data.result[id].authors
authors.map((author, idx) =>
idx > 0 ? authorList.push(" " + author.name) : authorList.push(author.name))
paperObj.title = title
paperObj.journal = journal
paperObj.authors = authorList.toString()
paperList.push(paperObj)
})
return paperObj
})
this.setState({papersList: list})
}
}
render () {
let article = ""
if (this.state.papersList.length){
article = this.state.papersList.map(paper =>
console.log (paper.title)
console.log (paper.authors)
console.log (paper.journal)
)
}
return (
<div className="App">
<h1>Publications</h1>
{article}
</div>
);
}
}
export default App;
I expect that when I map over paperList and extract each paper I should be able to return the title, journal or authors using console.log(paper.title), console.log(paper.title), console.log(paper.title). These are all returning undefined.
You have two issues in code
1) paperList array declaration should be out of map loop.
2) paperList should be returned instead of paperObj
Working code below make some enhancements in render function
Also codesandbox link
import React from "react";
import ReactDOM from "react-dom";
import rateLimit from "axios-rate-limit";
import axios from "axios";
import "./styles.css";
class App extends React.Component {
state = {
idlist: [],
papersList: ""
};
componentDidMount() {
console.log("incomponent");
axios
.get(
"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&retmode=json&retmax=1000&term=((Australia%5Bad%5D)%20AND%20(%222019%2F07%2F01%22%5BDate%20-%20Publication%5D%20%3A%20%223000%22%5BDate%20-%20Publication%5D))%20AND%20(%22nature%22%5BJournal%5D%20OR%20%22Nature%20cell%20biology%22%5BJournal%5D%20OR%20%22Nature%20structural%20%26%20molecular%20biology%22%5BJournal%5D)"
)
.then(response =>
this.setState({ idlist: response.data.esearchresult.idlist }, () => {
this.addPapers(this.state.idlist);
})
);
}
addPapers = idlist => {
if (idlist) {
const http = rateLimit(axios.create(), {
maxRequests: 6,
perMilliseconds: 1000
});
let paperList = [];
this.state.idlist.forEach(id => {
let paperObj = {};
http
.get(
`https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&rettype=abstract&id=${id}&api_key=9476810b14695bd14f228e63433facbf9c08`
)
.then(response2 => {
const title = response2.data.result[id].title;
const journal = response2.data.result[id].fulljournalname;
const authorList = [];
const authors = response2.data.result[id].authors;
authors.map((author, idx) =>
idx > 0
? authorList.push(" " + author.name)
: authorList.push(author.name)
);
paperObj.title = title;
paperObj.journal = journal;
paperObj.authors = authorList.toString();
paperList.push(paperObj);
})
.then(result => {
this.setState({ papersList: paperList });
});
});
}
};
render() {
return (
<div className="App">
<h1>Publications</h1>
{this.state.papersList.length &&
this.state.papersList.map(data => {
return <div>{data.title}</div>;
})}
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Hope it helps!!!
Do it like this:
render () {
let article;
if (this.state.papersList.length){
article = this.state.papersList.map(paper => <p>span>Title is {paper.title}</span></p> )
}
return (
<div className="App">
<h1>Publications</h1>
{article}
</div>
);
}