React lazy load won't work inside React perfect scroll bar - reactjs

I am using react-perfect-scrollbar to show images list.
Inside the perfect scroll bar, I am going to lazy load images. But it won't work.
import React, { useState, useCallback, useEffect } from 'react';
import PerfectScrollbar from 'react-perfect-scrollbar';
import useIsMountedRef from 'src/hooks/useIsMountedRef';
import LazyLoad from 'react-lazyload';
import {
Box,
Button,
Link,
List,
ListItem,
ListItemIcon,
ListItemText,
CircularProgress,
Typography,
makeStyles
} from '#material-ui/core';
const isMountedRef = useIsMountedRef();
const [images, setImages] = useState([]);
const getImages = useCallback(async () => {
try {
setLoading(true);
const response = await axios.get(`${backendUrl}/images/get/images`);
if (isMountedRef.current) {
setImages(response.data.projectImages);
}
} catch (err) {
console.error(err);
} finally {
if (isMountedRef.current) {
setLoading(false);
}
}
}, [isMountedRef]);
<PerfectScrollbar options={{ suppressScrollX: true }}>
<List className={classes.list}>
{images.map((image, i) => (
<ListItem
divider={i < images.length - 1}
key={i}
className={classes.listItem}
>
<ListItemIcon>
<LazyLoad height={90} key={i} overflow>
<img
src={`${awsS3Url}/${image.Key}`}
className={classes.listImage}
onClick={() => onSelect(`${awsS3Url}/${image.Key}`)}
/>
</LazyLoad>
</ListItemIcon>
<ListItemText
primary={GetFilename(image.Key)}
primaryTypographyProps={{ variant: 'h5' }}
secondary={bytesToSize(image.Size)}
className={classes.listItemText}
/>
<MoreButton
handleArchive={() => handleRemoveOne(image)}
/>
</ListItem>
))}
</List>
</PerfectScrollbar>
Some of images(the first view without scrolling) are showing.
When I scroll, the images won't load, only the list contents without images are showing.
What did I code wrong?

I had the same problem. The issue is that react-lazyload is trying to find a container that has its overflow property set to scroll or auto, but perfect-scrollbar sets its overflow property to hidden and handles the scrolling manually instead. So we have to tell react-lazyload manually which container it should monitor for scroll events.
This can, by documentation, be done in two ways; by passing an HTMLElement or a query selector string. Alas, there seems to be a bug in the library that causes the property to be ignored if it is not a string (https://github.com/twobin/react-lazyload/blob/055405125d0313014f0951cffc78345297f10a08/lib/index.js#L261) so currently the only way is to pass a query selector string.
But when I tried to pass a query selector string targeting the perfect-scrollbars container, it seems that the container might not always be there yet when react-lazyload attaches its event listeners, so we must check that the container is actually there before we initialize the LazyLoad-container.
So the relevant code is:
import React, { ReactElement, useRef, useState, useEffect } from 'react';
import PerfectScrollbar from 'react-perfect-scrollbar'
import LazyLoad from 'react-lazyload';
export default (): ReactElement => {
// Get a reference to the wrapper element so we know when it is created
const scrollbarWrapperRef = useRef(null);
// Initialize a state setter to notify the view when the scrollParent becomes available
const [scrollParent, setScrollParent] = useState<HTMLElement|null>(null);
// Adjust this selector to your liking
const scrollParentSelector = '#scrollbar-wrapper .scrollbar-container';
// Here we make sure that the PerfectScrollbar container is actually available before we let the content and the LazyLoads be created.
useEffect(() => {
const scrollParentElement = document.querySelector(scrollParentSelector);
if (scrollParentElement) {
setScrollParent(scrollParentElement);
}
}, [scrollbarWrapperRef.current]);
// The relevant DOM
return (
<div ref={scrollbarWrapperRef} id="scrollbar-wrapper">
<PerfectScrollbar>
{ scrollParent &&
<List>
{images.map((image, i) => (
<LazyLoad scrollContainer={scrollParentSelector}>
<img src="..." />
</LazyLoad>
))}
</List>
}
</PerfectScrollbar>
</div>
);
}

Related

Conditionally render component views based on browser screen size

I have three different components representing different views for screen sizes. I want to render one of them based on the browser view width. I've managed to get this working using ternary operators, but it'll only work with two elements.
Can someone let me know how to accomplish this without having to use display: hidden on one of the elements?
Here is my code so far:
import React, { useState, useEffect } from "react";
import Navbar from "../../shared/components/Navbar/Navbar";
import Page from "../../shared/interface/Page/Page";
{/* elements */}
import DesktopView from "../../home/components/DesktopView/DesktopView";
import MobileView from "../../home/components/MobileView/MobileView";
import TabletView from "../../home/components/TabletView/TabletView";
function Home() {
const [isDesktop, setDesktop] = useState(window.innerWidth > 650);
const [isTablet, setTablet] = useState(window.innerWidth > 768);
const [isMobile, setMobile] = useState(window.innerWidth > 640);
const updateMedia = () => {
setDesktop(window.innerWidth > 1024);
setTablet(window.innerWidth > 768);
setMobile(window.innerWidth > 640);
};
useEffect(() => {
window.addEventListener("resize", updateMedia);
return () => window.removeEventListener("resize", updateMedia);
});
return (
<Page>
<Navbar />
{/* Elements should go here */}
</Page>
);
}
export default Home;
You can try following Code instead of ternary
isDesktop && <DesktopView />
isTablet && <TabletView />
isMobile && <MobileView />
So if isDesktop is true then only DesktopView will render and similarly isTablet will control TabletView and isMobile will control MobileView rendering

React.js: How to close headlessui Disclosure modal from code?

I encountered an issue trying to close the headlessui Disclosure modal inside the panel.
My goal is to have a button inside the panel which can close the modal.
The way I tried to solve this problem is doing it manually using useRef, but it works partially.
After opening the panel for the first time, you can close the modal but if you try to open it again, it doesn't work. Can't figure out how to solve this issue.
Any help will be appreciated.
Here is the codesandbox link
And here is the code
import { Disclosure } from "#headlessui/react";
import React, { useState, useRef } from "react";
import CloseIcon from "#material-ui/icons/Close";
import ExpandMoreIcon from "#material-ui/icons/ExpandMore";
const App = () => {
const [isClosed, setIsClosed] = useState(false);
const modalRef = useRef(null);
const hideModalHandler = (e) => {
e.preventDefault();
modalRef.current?.click();
setIsClosed(!isClosed);
};
return (
<Disclosure>
{({ open }) => (
<div ref={modalRef}>
<Disclosure.Button>
<span>modal</span>
<ExpandMoreIcon />
</Disclosure.Button>
{!isClosed && (
<Disclosure.Panel>
<CloseIcon onClick={hideModalHandler} />
<div>name</div>
</Disclosure.Panel>
)}
</div>
)}
</Disclosure>
);
};
export default App;
I haven't used headlessui Disclosure but I see that the function hideModalHandler isn't actually hiding but toggling. Did you mean setIsClosed(true) instead of setIsClosed(!isClosed)?
Also, after a quick look at the documentation, have you tried using the close from the headlessui Disclosure? You don't need useRef
Use the state, and wrap the disclosure button into a DIV
with onClick and some ID string to identify what disclosure must be open. Something like this (works for multiple disclosures):
const [keyOfOpenDisclosure, setKeyOfOpenDisclosure] = useState('')
const toggleDisclosure = (key: string) => {
setKeyOfOpenDisclosure((prev) => (prev !== key ? key : ''))
}
...
<Disclosure>
<div onClick={() => toggleDisclosure(someId)}>
<Disclosure.Button>
Text of disclosure button
</Disclosure.Button>
</div>
<Transition
show={someId === keyOfOpenDisclosure}
...

Gatsby production build is different from local build

I have a weird result in my production build that is not replicated in my local development build. I'm using Gatsby with ReactBootstrap to make Tabs and a Carousel. I've coded the app so that if the screen width is less than 576, the carousel component is loaded, otherwise, the tab component is loaded.
So here's the problem. The first two HTML nodes in service_carousel.js i.e the Container and Carousel tags, are added on page load and become the wrapper for the service_tabs.js code. I don't know why. The services_tabs.js code should be loaded since I'm viewing it from a laptop screen and should only have the nodes specified in services_tabs.js. If I inspect the code and change the device to a phone, the error is fixed and the tags are removed even if I switch back to a large screen. However, if you reload the page the error comes back.
Here's a code sandbox with the full code https://codesandbox.io/s/sad-glade-u8j9g
My code is as follows:
service_tabs.js
import React from 'react';
import styles from './service_tabs.module.scss';
import { TabContent } from '../component_barrel';
import {
Tab,
Tabs,
Row,
Col,
} from '../../utils/bootstrap_imports_barrel';
import useData from '../../utils/useData';
const tab_data = useData.tab_data;
const ServiceTabs = () => (
<Row className="justify-content-center p-4">
<Col lg={10} md={9} className="align-self-center">
<Tabs justify className={styles.custom_tabs} defaultActiveKey="item 1" id="uncontrolled-tab-example">
{
tab_data.map(({ tab_title, title, icon, image, content }, index) => {
const key = `item ${index + 1}`;
return (
<Tab eventKey={key} key={key} title={tab_title}>
<TabContent
icon={icon}
image={image}
title={title}
content={content}
/>
</Tab>
)
})
}
</Tabs>
</Col>
</Row>
);
export default ServiceTabs;
service_carousel.js
import React from 'react';
import {
Container,
Carousel,
} from '../../utils/bootstrap_imports_barrel';
import styles from './service_carousel.module.scss';
import { TabContent } from '../component_barrel';
import useData from '../../utils/useData';
const tab_data = useData.tab_data;
const ServiceCarousel = () => (
<Container className="p-0" fluid>
<Carousel className="py-4" controls={false} indicators={false} touch={true}>
{
tab_data.map(({ title, icon, image, content }, index) => {
const key = `item ${index + 1}`;
return (
<Carousel.Item key={key} className={styles.carousel_container}>
<TabContent
icon={icon}
image={image}
title={title}
content={content}
/>
</Carousel.Item>
)
})
}
</Carousel>
</Container>
);
export default ServiceCarousel;
and the main service.js
import React from 'react';
import {
ServiceTabs,
ServiceCarousel
} from './component_barrel'
import { useWindowWidth } from '#react-hook/window-size';
const Service = () => {
const width = useWindowWidth();
const componentLoaded = width > 576 ? <ServiceTabs /> : <ServiceCarousel />;
return (
<div className="service_container">
{componentLoaded}
</div>
);
};
export default Service;
Since at the initial render point your code is asking for the window's width at const width = useWindowWidth();, your code will only work in the first load since the width of your window is set only one time and it is locked to that value until it re-renders. Your width will only apply to the first render.
To achieve what you are trying to do, you must check for the window availability first to await all your logic before it is set. That will cause a blink of a few milliseconds until the code calculates the window's width and choose what component render but it's the only way to do with any static site generator and window calculations. So, in your service.js:
import React from 'react';
import {
ServiceTabs,
ServiceCarousel
} from './component_barrel'
import { useWindowWidth } from '#react-hook/window-size';
const Service = () => {
let width;
if (typeof window !== 'undefined') width = useWindowWidth();
const width = useWindowWidth();
const componentLoaded = typeof window !== 'undefined' ? width > 576 ? <ServiceTabs /> : <ServiceCarousel /> : null;
return (
<div className="service_container">
{componentLoaded}
</div>
);
};
export default Service;
Note the duplicity of typeof window !== 'undefined', should be refactored to avoid repetitions but as an initial approach, it will do the job. In addition, a chained ternary condition is not the best option in terms of readability but for now, it will work.
Basically you are awaiting for the window creation to make your calculations and display a component or another based on the width value, that will be conditioned for the availability of the window.
You can check for further information about the window (and global objects) at Gatsby's documentation.

Changing button label on click with grommet?

Does anyone have any insight into changing button label on click when using grommet UI and styled components? Is it easier to just use a custom button rather than grommets?
Here are a few examples on how to make the Button label change onClick action:
import React, { useState } from "react";
import { render } from "react-dom";
import { grommet, Box, Button, Grommet, Text } from "grommet";
const App = () => {
const [label, setLabel] = useState(1);
const [name, setName] = useState("shimi");
const flipName = name => {
return name === "shimi" ? setName("shai") : setName("shimi");
};
return (
<Grommet theme={grommet}>
<Box pad="small" gap="small" width="small">
// label is a number that is being increased on every click event
<Button
label={label}
onClick={() => {
setLabel(label + 1);
}}
/>
// label string is controlled with external logic outside of the button.
<Button
label={<Text weight="400">{name}</Text>}
onClick={() => {
flipName(name);
}}
/>
</Box>
</Grommet>
);
};
render(<App />, document.getElementById("root"));
In addition to the examples above, in Grommet, you don't have to use the label prop and you can leverage the Button children to control the way your Button display.

Gatsby/React - fade out section on scroll?

I've been trying to use gatsby-plugin-scroll-reveal which uses Sal.js to animate a hero section on my site. I'm trying to make it so that the text in the hero fades in then fades out as you scroll down the page. Right now, I can only get it to fade in. How can I make that happen with Sal.js or another way?
I also tried a different way by creating a component that uses IntersectionObserver DOM API but I couldn't get that to work really.
Here's the component:
import React from 'react';
import ReactDOM from 'react-dom';
function FadeInSection(props) {
const [isVisible, setVisible] = React.useState(true);
const domRef = React.useRef();
React.useEffect(() => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => setVisible(entry.isIntersecting));
});
observer.observe(domRef.current);
return () => observer.unobserve(domRef.current);
}, []);
return (
<div
className={`fade-in-section ${isVisible ? 'is-visible' : ''}`}
ref={domRef}
>
{props.children}
</div>
);
}
export default FadeInSection
I figured out a solution from this article:
https://markoskon.com/scroll-reveal-animations-with-react-spring/
So, I'm using the react-spring to create reveal animations on scroll and react-visibility-sensor to see if the I want animated element is visible.
// App.js
import React from "react";
import { Spring } from "react-spring/renderprops";
import VisibilitySensor from "react-visibility-sensor";
<VisibilitySensor once>
{({ isVisible }) => (
<Spring delay={100} to={{ opacity: isVisible ? 1 : 0 }}>
{({ opacity }) => <h1 style={{opacity}}>Title</h1>}
</Spring>
)}
</VisibilitySensor>

Resources