Windowing in React/Gatsby/Chakra UI - reactjs

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

Related

Get the ref of an element rendering by an array

I'm coding a tab navigation system with a sliding animation, the tabs are all visible, but only the selected tab is scrolled to. Problem is that, I need to get the ref of the current selected page, so I can set the overall height of the slide, because that page may be taller or shorter than other tabs.
import React, { MutableRefObject } from 'react';
import Props from './Props';
import styles from './Tabs.module.scss';
export default function Tabs(props: Props) {
const [currTab, setCurrTab] = React.useState(0);
const [tabsWidth, setTabsWidth] = React.useState(0);
const [currentTabHeight, setCurrentTabHeight] = React.useState(0);
const [currentTabElement, setCurrentTabElement] = React.useState<Element | null>(null);
const thisRef = React.useRef<HTMLDivElement>(null);
let currentTabRef = React.useRef<HTMLDivElement>(null);
let refList: MutableRefObject<HTMLDivElement>[] = [];
const calculateSizeData = () => {
if (thisRef.current && tabsWidth !== thisRef.current.offsetWidth) {
setTabsWidth(() => thisRef.current.clientWidth);
}
if (currentTabRef.current && currentTabHeight !== currentTabRef.current.offsetHeight) {
setCurrentTabHeight(() => currentTabRef.current.offsetHeight);
}
}
React.useEffect(() => {
calculateSizeData();
const resizeListener = new ResizeObserver(() => {
calculateSizeData();
});
resizeListener.observe(thisRef.current);
return () => {
resizeListener.disconnect();
}
}, []);
refList.length = 0;
return (
<div ref={thisRef} className={styles._}>
<div className={styles.tabs}>
{ props.tabs.map((tab, index) => {
return (
<button onClick={() => {
setCurrTab(index);
calculateSizeData();
}} className={currTab === index ? styles.tabsButtonActive : ''} key={`nav-${index}`}>
{ tab.label }
<svg>
<rect rx={2} width={'100%'} height={3} />
</svg>
</button>
)
}) }
</div>
<div style={{
height: currentTabHeight + 'px',
}} className={styles.content}>
<div style={{
right: `-${currTab * tabsWidth}px`,
}} className={styles.contentStream}>
{ [ ...props.tabs ].reverse().map((tab, index) => {
const ref = React.useRef<HTMLDivElement>(null);
refList.push(ref);
return (
<div ref={ref} style={{
width: tabsWidth + 'px',
}} key={`body-${index}`}>
{ tab.body }
</div>
);
}) }
</div>
</div>
</div>
);
}
This seems like a reasonable tab implementation for a beginner. It appears you're passing in content for the tabs via a prop named tabs and then keeping track of the active tab via useState() which is fair.
Without looking at the browser console, I believe that React doesn't like the way you are creating the array of refs. Reference semantics are pretty challenging, even for seasoned developers, so you shouldn't beat yourself up over this.
I found a good article that discusses how to keep track of refs to an array of elements, which I suggest you read.
Furthermore, I'll explain the differences between that article and your code. Your issues begin when you write let refList: MutableRefObject<HTMLDivElement>[] = []; According to the React hooks reference, ref objects created by React.useRef() are simply plain JavaScript objects that are persisted for the lifetime of the component. So what happens when we have an array of refs like you do here? Well actually, the contents of the array are irrelevant--it could be an array of strings for all we care. Because refList is not a ref object, it gets regenerated for every render.
What you want to do is write let refList = React.useRef([]), per the article, and then populate refList.current with refs to your child tabs as the article describes. Referring back to the React hooks reference, the object created by useRef() is a plain JavaScript object, and you can assign anything to current--not just DOM elements.
In summary, you want to create a ref of an array of refs, not an array of refs. Repeat that last sentence until it makes sense.

gatsby-source-medium thumbnail image not showing

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.

Mapping Data Returning Same Value - Gatsby

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

Images Rerendering inside Styled Component when Chrome Dev Tools is open

This is a bit of a strange one and not sure why it's happening exactly.
When the component mounts, I call a function that in my application makes an HTTP request to get an array of Objects. Then I update 3 states within a map method.
enquiries - Which is just the response from the HTTP request
activeProperty - Which defines which object id is current active
channelDetails - parses some of the response data to be used as a prop to pass down to a child component.
const [enquiries, setEnquiries] = useState({ loading: true });
const [activeProperty, setActiveProperty] = useState();
const [channelDetails, setChannelDetails] = useState([]);
const getChannels = async () => {
// In my actual project,this is an http request and I filter responses
const response = await Enquiries;
const channelDetailsCopy = [...channelDetails];
setEnquiries(
response.map((e, i) => {
const { property } = e;
if (property) {
const { id } = property;
let tempActiveProperty;
if (i === 0 && !activeProperty) {
tempActiveProperty = id;
setActiveProperty(tempActiveProperty);
}
}
channelDetailsCopy.push(getChannelDetails(e));
return e;
})
);
setChannelDetails(channelDetailsCopy);
};
useEffect(() => {
getChannels();
}, []);
Then I return a child component ChannelList that uses styled components to add styles to the element and renders child elements.
const ChannelList = ({ children, listHeight }) => {
const ChannelListDiv = styled.div`
height: ${listHeight};
overflow-y: scroll;
overflow-x: hidden;
`;
return <ChannelListDiv className={"ChannelList"}>{children}</ChannelListDiv>;
};
Inside ChannelList component I map over the enquiries state and render the ChannelListItem component which has an assigned key on the index of the object within the array, and accepts the channelDetails state and an onClick handler.
return (
<>
{enquiries &&
enquiries.length > 0 &&
!enquiries.loading &&
channelDetails.length > 0 ? (
<ChannelList listHeight={"380px"}>
{enquiries.map((enquiry, i) => {
return (
<ChannelListItem
key={i}
details={channelDetails[i]}
activeProperty={activeProperty}
setActiveProperty={id => setActiveProperty(id)}
/>
);
})}
</ChannelList>
) : (
"loading..."
)}
</>
);
In the ChannelListItem component I render two images from the details prop based on the channelDetails state
const ChannelListItem = ({ details, setActiveProperty, activeProperty }) => {
const handleClick = () => {
setActiveProperty(details.propId);
};
return (
<div onClick={() => handleClick()} className={`ChannelListItem`}>
<div className={"ChannelListItemAvatarHeads"}>
<div
className={
"ChannelListItemAvatarHeads-prop ChannelListItemAvatarHead"
}
style={{
backgroundSize: "cover",
backgroundImage: `url(${details.propertyImage})`
}}
/>
<div
className={
"ChannelListItemAvatarHeads-agent ChannelListItemAvatarHead"
}
style={{
backgroundSize: "cover",
backgroundImage: `url(${details.receiverLogo})`
}}
/>
</div>
{activeProperty === details.propId ? <div>active</div> : null}
</div>
);
};
Now, the issue comes whenever the chrome dev tools window is open and you click on the different ChannelListItems the images blink/rerender. I had thought that the diff algorithm would have kicked in here and not rerendered the images as they are the same images?
But it seems that styled-components adds a new class every time you click on a ChannelListItem, so it rerenders the image. But ONLY when the develop tools window is open?
Why is this? Is there a way around this?
I can use inline styles instead of styled-components and it works as expected, though I wanted to see if there was a way around this without removing styled-components
I have a CODESANDBOX to check for yourselves
If you re-activate cache in devtool on network tab the issue disappear.
So the question becomes why the browser refetch the image when cache is disabled ;)
It is simply because the dom change so browser re-render it as you mentioned it the class change.
So the class change because the componetn change.
You create a new component at every render.
A simple fix:
import React from "react";
import styled from "styled-components";
const ChannelListDiv = styled.div`
height: ${props => props.listHeight};
overflow-y: scroll;
overflow-x: hidden;
`;
const ChannelList = ({ children, listHeight }) => {
return <ChannelListDiv listHeight={listHeight} className={"ChannelList"}>{children}</ChannelListDiv>;
};
export default ChannelList;
I think it has to do with this setting to disable cache (see red marking in image)
Hope this helps.

How to access the list of selected DropdownItemCheckboxes in #atlaskit/dropdown-menu

There is no documentation or examples for DropdownMenu which show how to obtain and react to modifications to the state of DropdownItemCheckbox items. Some of the relevant features seem to have been deprecated. How is this done now?
I would like the user to be able to see a list of sprints, epics and version defined in Jira and show the filtered content of the board (i.e duplicating some of the functions of the Backlog screen). To do that the user must select what they are interested in so my back-end can go and do it's work over that selection.
You should be able to track the selection/unselection of items by putting an onClick handler on each <DropdownItemCheckbox> and storing the selected status of each dropdown item in React state - it looks like that's not documented at https://atlaskit.atlassian.com/packages/core/dropdown-menu
I've put together a quick live demo at https://codesandbox.io/s/9jqyk1pw5o and the code is below as well.
import React from "react";
import { render } from "react-dom";
import Dropdown, {
DropdownItemCheckbox,
DropdownItemGroupCheckbox
} from "#atlaskit/dropdown-menu";
import "#atlaskit/css-reset";
class App extends React.Component {
state = {
selectedItems: ["js"]
};
handleSelection = id => () => {
const { selectedItems } = this.state;
if (selectedItems.includes(id)) {
this.setState({
selectedItems: selectedItems.filter(item => item != id)
});
} else {
this.setState({
selectedItems: [...selectedItems, id]
});
}
};
render() {
const { selectedItems } = this.state;
return (
<div style={{ padding: 40 }}>
<p>Selected items: {selectedItems.join(", ")}</p>
<Dropdown defaultOpen triggerType="button" trigger="Drop menu">
<DropdownItemGroupCheckbox id="languages2" title="Languages">
{["js", "java", "ruby"].map(id => (
<DropdownItemCheckbox
id={id}
key={id}
isSelected={selectedItems.includes(id)}
onClick={this.handleSelection(id)}
>
{id}
</DropdownItemCheckbox>
))}
</DropdownItemGroupCheckbox>
</Dropdown>
</div>
);
}
}
render(<App />, document.getElementById("root"));
There is also a developer preview of a Select component https://atlaskit.atlassian.com/packages/core/select

Resources