I'm mapping over some data (3 image slides i have stored in a headless cms). I am passing the retuned data into slide component, and from there its being passed down to a button component, which is just a 'gatsby-link'. The mapped data contains a "page_link", among other things that should be different for each slide. However, for some reason the same "page_link" is being passed down to each button, even though they should all be different. I am logging my data in the console and can confirm that each array i am mapping over contains different data. I get the different image, title, text, etc fields for each slide, but i am recieving the same "page_link" for each slide. I am confused as to what i am doing wrong.
Heres my code:
GraphQl Query:
const HOMEPAGE_DATA = graphql`
query {
prismicHomePage {
data {
body {
slider {
title {
text
}
text {
text
}
page_link{
text
}
button_text{
text
}
image {
localFile {
childImageSharp {
fluid(maxWidth: 1280, quality: 90) {
...GatsbyImageSharpFluid_withWebp
}
}
}
}
}
}
}
}
`
// get homepage data
const data = useStaticQuery(HOMEPAGE_DATA)
// simplify data
const home_data = data.prismicHomePage.data
Mapped Data:
{home_data.slider.map((slide, index) => {
console.log(slide)
return (
<Slide
key={slide.title.text}
title={slide.title.text}
image={slide.image.localFile.childImageSharp.fluid}
text={slide.text.text}
button_text={slide.button_text.text}
path_link={slide.page_link.text}
/>
)
})}
Slide Component:
const Slide = props => {
return (
<StyledBackgroundImage fluid={props.image} alt={`${props.title}`}>
<Wrapper>
<SlideContent>
<h1>{props.title}</h1>
<p>{props.text}</p>
<div className="slide-btn">
<Button path={props.path_link} text={props.button_text} />
</div>
</SlideContent>
</Wrapper>
</StyledBackgroundImage>
)
}
Button Component:
The is just a Styled "Link" Component from Gatsby.
const Button = props => {
return (
<StyledButton to={props.path} title={`${props.text}`}>
{props.text}
</StyledButton>
)
}
Related
This question already has answers here:
Pass props to parent component in React.js
(7 answers)
get data from child components to parents components in react js
(3 answers)
Closed 8 days ago.
I have a component that autocompletes places on my page that manages To and From destinations. Within the component, there are additional utilities, for example returning the geocoding of the given position.
How can I pass back the geocoded data back to the page? Anybody has any idea on best practice patterns to do this?
Example component: PlacesAutocomplete.jsx from https://github.com/wellyshen/use-places-autocomplete#create-the-component
import usePlacesAutocomplete, {
getGeocode,
getLatLng,
} from "use-places-autocomplete";
import useOnclickOutside from "react-cool-onclickoutside";
const PlacesAutocomplete = () => {
const {
ready,
value,
suggestions: { status, data },
setValue,
clearSuggestions,
} = usePlacesAutocomplete({
callbackName: "YOUR_CALLBACK_NAME",
requestOptions: {
/* Define search scope here */
},
debounce: 300,
});
const ref = useOnclickOutside(() => {
// When user clicks outside of the component, we can dismiss
// the searched suggestions by calling this method
clearSuggestions();
});
const handleInput = (e) => {
// Update the keyword of the input element
setValue(e.target.value);
};
const handleSelect =
({ description }) =>
() => {
// When user selects a place, we can replace the keyword without request data from API
// by setting the second parameter to "false"
setValue(description, false);
clearSuggestions();
// Get latitude and longitude via utility functions
getGeocode({ address: description }).then((results) => {
const { lat, lng } = getLatLng(results[0]);
console.log("📍 Coordinates: ", { lat, lng });
});
};
const renderSuggestions = () =>
data.map((suggestion) => {
const {
place_id,
structured_formatting: { main_text, secondary_text },
} = suggestion;
return (
<li key={place_id} onClick={handleSelect(suggestion)}>
<strong>{main_text}</strong> <small>{secondary_text}</small>
</li>
);
});
return (
<div ref={ref}>
<input
value={value}
onChange={handleInput}
disabled={!ready}
placeholder="Where are you going?"
/>
{/* We can use the "status" to decide whether we should display the dropdown or not */}
{status === "OK" && <ul>{renderSuggestions()}</ul>}
</div>
);
};
Example page: index.jsx
import { PlacesAutocomplete } from #/components/PlacesAutocomplete
export default function Example() {
return (
<div className="bg-white">
<PlacesAutocomplete />
<PlacesAutocomplete /> {/* I want to return the long and lat values here */}
<div>
}
The component is used multiple times on the page. I have considered creating the input fields directly on the page but it seems counterintuitive that I will have to intialize the autocomplete input multiple times.
So I have a filter chip, and this filter chip is just passed a text body, and close function like so:
import CloseIcon from '#mui/icons-material/Close';
import "./FilterChip.css";
function FilterChip({textBody, onCloseClick}) {
return <div className="filter-chip">
Category: {textBody} <CloseIcon onClick={onCloseClick} className="filter-chip-close-button"/>
</div>
}
export default FilterChip;
I can render multiple filter chips in one page. How can I tell my parent component that the particular chip's x button has been clicked? Is it possible to pass this data on the onCloseClick function? I need to remove the chip once it's x button has been clicked, and I also need to uncheck it from my list of check boxes in my parent component. This is how I render the chips.
function renderFilterChips() {
const checkedBoxes = getCheckedBoxes();
return checkedBoxes.map((checkedBox) =>
<FilterChip key={checkedBox} textBody={checkedBox} onCloseClick={onChipCloseClick} />
);
}
You should pass an "identifier" for each chip and then use that identifier to find out "what" was clicked by the user. And then you can filter out the clicked chip.
function FilterChip({ textBody, onCloseClick, id }) {
const handleOnClose = (event) => {
onCloseClick(event, id);
};
return (
<div className="filter-chip">
Category: {textBody}{" "}
<CloseIcon onClick={handleOnClose} className="filter-chip-close-button" />
</div>
);
}
Now your onCloseClick should accept a new param id and handle the logic to remove the chip .
Hope it helps.
Sounds like you need checkedBoxes to be in state.
import { useState } from "react"
const initialBoxes = getCheckedBoxes()
function renderFilteredChips() {
const [ checkedBoxes, setCheckedBoxes ] = useState(initialBoxes)
}
Then implement a function to remove a checked box by its index (or if you have a unique key identifier that would be even better)
const onChipCloseClick = (indexToRemove) => {
setCheckedBoxes(state => state.filter((_, chipIndex) => chipIndex !== indexToRemove))
}
Then when you map over the chips, make sure the function that closes the chip has its index, effectively allowing each chip in state to filter itself out of state, which will re-render your chips for you.
import { useState } from "react"
const initialBoxes = getCheckedBoxes()
function renderFilteredChips() {
const [ checkedBoxes, setCheckedBoxes ] = useState(initialBoxes)
const onChipCloseClick = (indexToRemove) => {
setCheckedBoxes(state => state.filter((_, chipIndex) => chipIndex !== indexToRemove))
}
return <>
{checkedBoxes.map((checkedBox, index) => (
<FilterChip
key={index}
textBody={checkedBox}
onCloseClick={() => onChipCloseClose(index)}
/>
})
</>
}
Obligatory note that I haven't checked this and wrote it in Markdown, so look out for syntax errors (:
I have a simple web app using React, Gatsby and Chakra UI frameworks. The app consists of an index page which queries frontmatter from 1000+ mdx files and renders a minimal summary component with 5 fields from the frontmatter and a link to a detail page for each. The 1000+ detail pages are generated in gatsby-node.js using createPage.
The index page uses map to iterate through each of the mdx nodes and uses a Chakra UI simple grid along with some other Chakra components for each item.
The lighthouse report received when the app is deployed to Gatsby Cloud rates the app 60/100 for performance largely due to excessive elements in the DOM (the 1000+ summary elements rendered by the index page).
I’ve reviewed all of the related documentation and searched SO among other sources but can find no feasible solution to rendering only the html for the 25 or so items that are displayed on screen at any given point and rendering the rest as needed rather than rendering all 1000+ from the outset.
import * as React from "react";
import { ChakraProvider, chakra, Box, SimpleGrid, HStack, Button, VStack, Wrap, WrapItem, Badge } from "#chakra-ui/react";
import { graphql, useStaticQuery } from 'gatsby';
const IndexPage = () => {
const query = useStaticQuery(graphql`
query AllObjects {
allMdx(sort: {fields: frontmatter___field1}) {
nodes {
frontmatter {
field1
field2
field3
field4
field5
field6
field7
uniqueId
}
}
}
}
`)
return (
<ChakraProvider>
<body>
<main>
<SimpleGrid columns={{base: 1, lg: 3, md: 2, sm:1}} spacing={{base: '1.5em', lg: '1.5em', md:'1.0', sm:'0.90em'}}>
{ query.allMdx.nodes.map((node) => (
<Box key={node.frontmatter.field1} margin="2em" padding="1em">
<HStack padding="0.4em" align="center" alignItems="stretch" justifyContent="space-between">
<Button size="sm" shadow="md" colorScheme="blue"
onClick={(e) => {
e.preventDefault();
window.location.href=`/objects/${node.frontmatter.field1.toLowerCase()}`;
}}
>Detail</Button>
<Box align="center"/>
<VStack alignItems="end" justifyContent="right">
<Wrap columns={2} spacing={1} direction={["row-reverse"]} isInline="true" shouldWrapChildren="true">
<WrapItem>
{node.frontmatter.field5 === true &&
<Badge colorScheme="green">Field5</Badge>
}
</WrapItem>
<WrapItem>
{node.frontmatter.field6 === true &&
<Badge colorScheme="blue">Field6</Badge>
}
</WrapItem>
<WrapItem>
{node.frontmatter.field7 === true &&
<Badge colorScheme="orange">field7</Badge>
}
</WrapItem>
<WrapItem>
{node.frontmatter.field4 === true &&
<Badge colorScheme="red">Field8</Badge>
}
</WrapItem>
</Wrap>
</VStack>
</HStack>
<Box bg="gray.300" borderRadius="0.5em" margin="0em" padding="0em">
<chakra.h2 id={node.frontmatter.field1.toLowerCase()}>
Field1: {node.frontmatter.field1}
</chakra.h2>
<chakra.p>Field2: {node.frontmatter.field2}</chakra.p>
<chakra.p>{node.frontmatter.field3}</chakra.p>
</Box>
</Box>
))}
</SimpleGrid>
</main>
</body>
</ChakraProvider>
)
};
export default IndexPage;
Well, you spot the solution. Use an infinite scroll or some similar delayed (button, etc) approach to render the full amount of grid items on-demand, rather than all of them at the same time.
Just create a state (useState) that contains the sliced amount of elements and upgrade them as soon as the user scrolls the page. That will save (and delay) your initial DOM elements.
I will add a button-based approach to render more elements but the idea is exactly the same using an infinite scroll.
const IndexPage = () => {
const query = useStaticQuery(graphql`
query AllObjects {
allMdx(sort: {fields: frontmatter___field1}) {
nodes {
frontmatter {
field1
field2
field3
field4
field5
field6
field7
uniqueId
}
}
}
}
`)
// Array of all news articles
const allGridElements = query.allMdx.nodes
// State for the list
const [list, setList] = useState([...allGridElements.slice(0, 10)])
// State to trigger the load more
const [loadMore, setLoadMore] = useState(false)
// State of whether there is more to load
const [hasMore, setHasMore] = useState(allGridElements.length > 10)
// Load more button click
const handleLoadMore = () => {
setLoadMore(true)
}
// Handle loading more articles
useEffect(() => {
if (loadMore && hasMore) {
const currentLength = list.length
const isMore = currentLength < allGridElements.length
const nextResults = isMore
? allGridElements.slice(currentLength, currentLength + 10)
: []
setList([...list, ...nextResults])
setLoadMore(false)
}
}, [loadMore, hasMore]) //eslint-disable-line
//Check if there is more
useEffect(() => {
const isMore = list.length < allGridElements.length
setHasMore(isMore)
}, [list]) //eslint-disable-line
return (
<div>
<h1>Load more demo</h1>
<div>
{list.map((item) => (
{ /* Your JSX rendering the grid items */ }
))}
</div>
{hasMore ? (
<button onClick={handleLoadMore}>Load More</button>
) : (
<p>No more results</p>
)}
</div>
)
}
export default IndexPage
Note: to avoid a neverending answer I omitted the JSX returned by your loop. Just place it in the comment.
It's quite self explanatory, you set all your elements in a React state (useState) and iterate through it. The useEffect is in charge of upgrade the list based on a listener.
Other useful resources:
https://scotch.io/tutorials/build-an-infinite-scroll-image-gallery-with-gatsby-and-netlify-functions
https://www.gatsbyjs.com/starters/baobabKoodaa/gatsby-starter-infinite-scroll
My image component displays images with a heart over it every time a user submits a search. The heart changes colors if the image is clicked, and should reset to white (default color) when user submits a new search. For some reason, the clicked-color persists even after a search. What am I not understanding about react states? This isn't simply something that changes on the next render. It just stays like that until I change it manually.
const Image = ({image, toggleFav, initialIcon, initialAlt}) => {
const [fav, setFav] = useState(false);
const [heartIcon, setHeartIcon] = useState(initialIcon)
const [heartAlt, setHeartAlt] = useState(initialAlt)
const handleClick = () => {
setFav(fav => !fav);
toggleFav(image.id, fav);
if (heartIcon == "whiteHeartIcon") {
setHeartIcon("redHeartIcon")
}
else {
setHeartIcon("whiteHeartIcon")
}
if (heartAlt == "white heart icon") {
setHeartAlt("red heart icon")
}
else {
setHeartAlt("white heart icon")
}
};
return (
<Grid item xs={4} key={image.id}>
<div className={`${fav ? "fav" : ""}`} onClick={handleClick}>
<div className="imgBox">
<img src={image.url} className="image"/>
<Heart icon={heartIcon} alt={heartAlt} className="heart"/>
</div>
</div>
</Grid>
);
}
This is the handle submit func for the component:
const searchAllImages = async (keyword) => {
const response = await searchImages(keyword);
const imageObjects = response.data.message.map((link, index) => {
let newImage = {
url: link,
id: link,
fav: false
};
return newImage;
});
dispatch({type: 'SET_IMAGES', payload: imageObjects});
};
I render the images through a redux store where it replaces the image state every time a new search is done. The state resides in Store.js where image is initially set to an empty list. The dispatch method comes from Reducer.js where the method is defined.
case "SET_IMAGES":
return {
...state,
images: action.payload
}
Have you tried setting the initial image to a different variable initially, then use a useEffect that checks the image variable for changes, and if it changes assign the value to the variable mentioned above. Anyways more like using useEffect or useCallback for actions.
I'm using gatsby in my react project, to show my medium, articles inside the project.
below is my graphql query for that.
const BlogPost = () => {
const blogMediumQueryData = useStaticQuery(graphql`
query Medium {
allMediumPost(sort: { fields: [createdAt], order: DESC }) {
edges {
node {
id
title
uniqueSlug
createdAt(formatString: "MMM YYYY")
virtuals {
previewImage {
imageId
}
}
author {
name
}
}
}
}
}
`)
const blogs = blogMediumQueryData.allMediumPost.edges
return (
<Blog
image={blog.node.virtuals.previewImage.imageId}
title={blog.node.title}
date={blog.node.createdAt}
author={blog.node.author.name}
path={blog.node.uniqueSlug}
/>
)
this gives me the preview image ID. And I'm passing it to the child component as a prop. But when I try to show the image with the Img component from gatsby, the Image is not showing.
Here is my code for the child component
import React from "react"
import { Link } from "gatsby"
import { slugify } from "../utils/utilityFunctions"
import Image from "../elements/image"
const Blog = ({ image }) => {
return (
<div className="content-block">
<div className="post-thubnail">
{image && (
<Link to={postUrl} target='blank'>
<Image src={image} alt={title} />
</Link>
)}
</div>
)
}
export default Blog
Here is the code for the Image component
import React from "react";
import Img from "gatsby-image";
const NonStretchedImage = props => {
let normalizedProps = props
normalizedProps = {...normalizedProps.fluid, aspectRatio: 1}
let alignment;
if(props.align === 'right'){
alignment = '0 0 0 auto'
} else if(props.align === 'left'){
alignment = '0 auto 0 0'
}else{
alignment = '0 auto'
}
if (props.fluid && props.fluid.presentationWidth) {
normalizedProps = {
...props,
style: {
...(props.style || {}),
maxWidth: props.fluid.presentationWidth,
margin: alignment,
},
}
}
return <Img {...normalizedProps} />
}
export default NonStretchedImage;
This is my first project with gatsby and graphql. Is there are anything that I have missed or is there anything that I'm doing wrong?
Thanks in advance
A few caveats that I guess will put you on the track to fix the issue.
node, in the GraphQL query is an array, in the same way, I guess that virtuals it is. Check and test the response in the localhost:8000/___graphql playground.
So assuming that your query works as expected, your code should look like:
const BlogPost = () => {
const blogMediumQueryData = useStaticQuery(graphql`
query Medium {
allMediumPost(sort: { fields: [createdAt], order: DESC }) {
edges {
node {
id
title
uniqueSlug
createdAt(formatString: "MMM YYYY")
virtuals {
previewImage {
imageId
}
}
author {
name
}
}
}
}
}
`)
const blogs = blogMediumQueryData.allMediumPost.edges
return (
<Blog
image={blog.node[0].virtuals.previewImage.imageId}
title={blog.node[0].title}
date={blog.node[0].createdAt}
author={blog.node[0].author.name}
path={blog.node[0].uniqueSlug}
/>
)
Alternatively, you can loop through the array of nodes and use your previous Blog component since it will get each iterable variable.
I don't think your Image component be able to render a gatsby-image only using the imageId. Gatsby needs a bunch of data (given by its transformers and sharps) to render the image, not using an identifier but series of fields (that's why it usually renders query fragments, noted by ...). Your image component, in the end, should render something like:
<img src={`https://medium.com/${blog.node[0].virtuals.previewImage.imageId}`}
Based on: https://blog.devgenius.io/how-to-scrap-your-medium-articles-with-gatsby-js-f35535ebc09d
So summarizing, gatsby-source-medium by itself doesn't provide enough data to use gatsby-image or gatsby-image-plugin plugins so I'm afraid you won't be able to use the Img component. You have to use the standard img tag.