Started learning React and Next just some time ago and having trouble making this work.
Also tried using useLazyQuery but could not figure out how to return properly.
...
const TaskSingle = () => {
const { category } = router.query;
const { loading, error, data } = useQuery(GET_TASKS, {
variables: {
taskFilter: {
taskCategory: 5, // works
// taskCategory: category // doesn't work
},
},
});
}
const { categories } = data;
return (
<div>
{categories.map((cat) => {
<div key={cat.id}>
{cat.name}
</div>
})}
</div>
)
When I try to use "category" instead of a number I'm getting this error:
http://hidden-url:3000/graphql:1 Failed to load resource: the server responded with a status of 400 (Bad Request)
In the end I used
const category = Number(router.query.category);
instead of
const { category} = router.query;
because I needed a number and not a string.
Related
I am kinda new at react and trying to make two axios get call according to results of first call as you can see in my code. Eventually I am trying to .map the response data in swiper component. However my code is not working properly. The problem is, the data from the first call is displayed on the component properly while the images from the second call are not.
const [picks, setPicks] = useState([]);
const getAllPicks = async () => {
try {
axios.get(".../shoppicks").then(async (response) => {
const responseData = await response.data.result;
for (let i = 0; i < responseData.length; i += 1) {
axios.post(".../getimage", { shopID: response.data.result[i]._id }).then((res) => {
if (res.data.header.arg === "No image found") {
// dummy photo for corrupted responses
responseData[i].imgPath = "#"
} else {
responseData[i].imgPath = res.data
}
})
}
return responseData
}).then((responseData) => {
setPicks(responseData)
console.log(responseData) row 38 logs the data properly
console.log(picks) row 39 logs empty array
})
.catch((error) => console.log(JSON.stringify(error)));
}
catch (error) {
console.log(error)
};
}
useEffect(() => {
getAllPicks();
}, []);
Here where I try .map the data
{picks?.map((pick: IHomePickType) => (
<><SwiperSlide key={pick._id}>
<CardMedia image={pick.imgPath} component="img" />
<div>
<h2>{pick._id}</h2>
</div>
</SwiperSlide></>
))}
Additionally it throws "Each child in a list should have a unique "key" prop." console error even though the picks data has unique keys
I've updated my code according the comments. It is working now.
here is the final code
const [picks, setPicks] = useState([]);
const getAllPicks = async () => {
try {
const response = await axios.get("../shoppicks");
const data = await response.data.result;
for (let i = 0; i < data.length; i += 1) {
const imgResponse = await axios.post("../getimage", { shopID: data[i]._id });
if (imgResponse.data.header.arg === "No image found") {
// dummy photo for corrupted responses
data[i].imgPath = "#"
} else {
data[i].imgPath = imgResponse.data.result.imgPath
}
}
setPicks(data)
}
catch (error) {
console.log(error)
};
}
component =>
{picks?.map((pick: IHomePickType) => (
<SwiperSlide key={pick._id}>
<CardMedia image={pick.imgPath} component="img" />
<div>
<h2>{pick._id}</h2>
</div>
</SwiperSlide>
))}
Thanks
I'm using Strapi to call dynamic data into my website via an API GET request, and I want to generate paths for my dynamic pages. One level of dynamic pages works fine, but the second is a challenge.
My structure is as follows:
[category].js
[category]/[client].js
Both are dynamic, so I have, for example, a category "fashion" with multiple clients. The same goes for other categories like "products".
The first dynamic page works fine in building paths
[dynamic.js].
import CategoryCard from "../../../components/portfolio/categoryCard";
import { fetcher } from "../../../lib/api";
export const getStaticPaths = async () => {
const categoryPathResponse = await fetcher(
`${process.env.NEXT_PUBLIC_STRAPI_URL}/categories`
);
const data = categoryPathResponse.data;
const paths = data.map((path) => {
return {
params: { category: path.attributes.path.toString().toLowerCase() },
};
});
return {
paths,
fallback: false,
};
};
export async function getStaticProps(context) {
const category = context.params.category;
const categoryPropsResponse = await fetcher(
`${process.env.NEXT_PUBLIC_STRAPI_URL}/categories?filters[path][$eq]=${category}&?populate[0]=clients&populate[1]=clients.thumbnail`
);
return {
props: { category: categoryPropsResponse },
};
}
const CategoryOverviewPage = ({ category }) => {
const data = category.data;
const categoryTitle = data[0].attributes.Category;
return (
<>
{console.log('data for category before card', data)}
<div className="flex px-4 mt-24 lg:mt-12 lg:px-20">
<div>
<h1 className="[writing-mode:vertical-lr] [-webkit-writing-mode: vertical-lr] [-ms-writing-mode: vertical-lr] rotate-180 text-center">
{categoryTitle}
</h1>
</div>
<div className="grid grid-cols-[repeat(auto-fit,_minmax(150px,_250px))] gap-4 lg:gap-8 ml-4 lg:ml-32 max-w-[82vw]">
<CategoryCard data={data} />
</div>
</div>
</>
);
};
export default CategoryOverviewPage;
But the complexity comes with the second part, in which I have to create multiple paths per category. I tried and ended up with the following
[clients].js
export const getStaticPaths = async () => {
const categoryPathResponse = await fetcher(
`${process.env.NEXT_PUBLIC_STRAPI_URL}/categories?populate=*`
);
const data = categoryPathResponse.data;
const paths = data.map((path) => {
const category = path.attributes.path.toString().toLowerCase()
const client = path.attributes.clients.map((client) => client.name).toString().toLowerCase().replace(/\s+/g, "-")
return {
params: {
category: category, client: client
},
};
});
return {
paths,
fallback: false,
};
};
export async function getStaticProps(context) {
const category = context.params.category;
const client = context.params.client;
const data = await fetcher(
`${process.env.NEXT_PUBLIC_STRAPI_URL_BASE}/categories?filters[path][$eq]=${category}&?populate[clients][populate]=*&populate[clients][filters][name][$eq]=${client}`
);
return {
props: { client: data },
};
}
It seems to work for categories with only 1 item, which makes sense because a URL (path) is created like index/category/client.
But when there are multiple clients, it tries to create a path with 1 category and multiple clients attached to the same path, something like this category/client1client2.
This has to be separated, and for each client, there has to be a new path created like category1/client1, category1/client2, category2/client1, category2/client2, etc.
Any ideas?
In addition to mapping over the categories data, you also need to map over the clients array and generate a path entry for each.
Modify the code inside getStaticPaths in /[category]/[client].js as follows.
export const getStaticPaths = async () => {
// Existing code...
const paths = data.map((path) => {
const category = path.attributes.path.toString().toLowerCase()
return path.attributes.clients
.map((client) => {
const clientDetails = client.name.toLowerCase().replace(/\s+/g, "-")
return {
params: {
category: category, client: clientDetails
}
};
})
}).flat() // Flatten array to avoid nested arrays;
return {
paths,
fallback: false,
};
};
I am getting an error from strapi backend data when I fetch particular data into the front end react app I get an error when I log the data into the browser console I see my data in the browser console.
query GetPromoProducts {
allStrapiProduct(filter: {promo: {eq: true}}) {
edges {
node {
strapiId
name
variants {
images {
url
}
}
}
}
}
}
`)
const [selectedSlides, setSelectedSlides] = useState(0)
console.log(data);
var slides = [];
data.allStrapiProduct.edges.map(({node}) => {
console.log(node.variants[0].images[0]);
})
Debugging
To debug the server side, use optional chaining and console.log to work your way up to the object's that returning null.
Not sure what the exact code looks like, based on question.
const myQuery = graphql`
query GetPromoProducts {
allStrapiProduct(filter: { promo: { eq: true } }) {
edges {
node {
strapiId
name
variants {
images {
url
}
}
}
}
}
}
`;
const Component = () => {
const [selectedSlides, setSelectedSlides] = useState(0);
console.log(data);
data.allStrapiProduct.edges.map(({ node }) => {
// For this case, use optional chaining to work your way up the tree
console.log(node);
console.log(node?.variants);
console.log(node?.variants?.[0]);
console.log(node?.variants?.[0]?.images);
console.log(node?.variants?.[0]?.images?.[0]);
});
};
To stop right at the area this happens:
data.allStrapiProduct.edges.map(({ node }) => {
if (!node?.variants?.[0]?.images?.length) {
console.log(node);
console.log(node?.variants);
console.log(node?.variants?.[0]);
console.log(node?.variants?.[0]?.images);
console.log(node?.variants?.[0]?.images?.[0]);
throw "Required data missing";
}
});
Resilient React with GraphQL
GraphQL structured data may be typed, but unless the fields are required, you have to handle null.
To make the client-side code more robust, you can use Array.prototype.filter() to skip objects with no images, or - as in this example - make components handle the case where data is missing. It's entirely up to how you want the frontend app to render it.
I haven't tested this code, so take it as "pseudo-JSX" to demonstrate handling empty graphql response fields.
const ProductVariant = ({ variant }) => {
if (!variant) return null;
return (
<div className="variant">
{variant?.images?.map?.((image, idx) => (
<img src={url} key={idx} />
))}
</div>
);
};
const ProductVariants = ({ variants }) => {
if (!variants?.length > 0) return null;
return (
<div className="variant-list">
<div>Variants</div>
<div className="variant-list--items">
{variants?.map?.((variant, idx) => (
<ProductVariant variant={variant} key={idx} />
))}
</div>
</div>
);
};
const Products = () => {
const [selectedSlides, setSelectedSlides] = useState(0);
console.log(data);
const products = data.allStrapiProduct.edges.map(({ node }) => {
// Handle as you deem fit
});
return (
<div id="products">
<p>Here is a list of products</p>
<div className="products">
{products?.map?.((product, idx) => {
return (
<div className="product" key={idx}>
{product?.variants?.length > 1 && (
<ProductVariants variants={variants} />
)}
</div>
);
})}
</div>
</div>
);
};
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 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/