I have a component Scroller
which I don't control which takes in data as a prop.
This data is a list of objects. Within object, one of the keys takes in a function.
This component has ability where upon clicking on the square, I am meant to show a new component (like a pop up).
The component Scroller which I don't control taking in the data prop.
<Scroller
data={getData(allData)}
/>
This is the data being passed in. content is a list of objects.
const getData = (content) => content.map((c, i) => ({
header: c.header,
customOnClick: (() => {
setClicked(true); // this is the line which resets the scroll
}),
}
));
So this works as intended. Upon clicking, the new pop up content shows. This is due to state change via the setClicked
function.
The issue is that this Scroller component has a scroll option. So user could have scrolled pass a a block (0) like following image.
But the moment I click the button to show the popup, it resets the scroll position back to 0 like following. Instead of remaining in position as above.
This scroll reset is the issue.
This is being caused by the call to setClicked function. It doesn't matter if I do anything with it. As long as I call it, it resets.
Showing the popup component is not the issue. The mere call to setClicked is the issue.
Thus wondering if there a way I could toggle showing the pop up component without having to set state?
Or a way to maintain the scroll position without resetting the scroll.
Note that in this instance I am using hooks. It is the same outcome if I use Redux. Please advice.
This is my component which I can control.
import React, { Fragment } from 'react';
import Scroller from 'comp-external-lib';
import PopUpComponent from './PopUpComponent';
const MyComponent = ({data}) => {
const [isClicked, setClicked] = React.useState(false);
const { allData } = data;
const getData = (content) => content.map((c, i) => ({
header: c.header,
customOnClick: c.customOnClick && (() => {
setClicked(true); // this is whats causing the reset for scroll
}),
}
));
return (
<Fragment>
<Scroller
data={getData(allData)}
/>
{
{/* Doesn't matter if this is commented out. The scrolling will still reset due to call to setClicked function */}
{/* isClicked && <PopUpComponent /> */}
}
</Fragment>
);
};
export default MyComponent;
UPDATED:
This is the Scroller Component.
There is also a related scrolling logic component below.
That may be also relevant.
slides
is the data that is being passed in.
import React, { MouseEvent } from "react";
import styles from "./index.less";
import Element from "components-core/src/Element";
import Box from "components-atom-box/src";
import Typography from "components-atom-typography/src";
import { Color, Align, Size, FontType } from "components-core/src/enums";
import { Column, Row } from "components-atom-grid/src";
import Breakpoint from "components-styleless-breakpoint/src";
import ClickableIcon from "components-molecule-clickable-icon/src";
import AccelerationFilm from "./acceleration-film";
import { SupportedHtmlTags } from "components-core/src/types";
type Slide = {
key?: string;
header?: React.ReactNode;
subHeader?: React.ReactNode;
title?: string;
text: string;
className?: string;
horizontalVariant?: boolean;
customization?: Customization;
popoverOnClick?: (event: MouseEvent<HTMLElement>) => void;
};
interface Customization {
headerTag?: keyof SupportedHtmlTags;
headerColor?: Color;
subHeaderTag?: keyof SupportedHtmlTags;
subHeaderColor?: Color;
titleTag?: keyof SupportedHtmlTags;
titleColor?: Color;
textTag?: keyof SupportedHtmlTags;
textColor?: Color;
fontSizes?: {
headerFontSize: number;
subHeaderFontSize: number;
};
}
interface Props {
data: {
slides: Slide[];
};
showFloatingBanner?: boolean;
customization?: Customization;
$pos?: number;
}
const defaultCustomization: Customization = {};
const SingleSlide = ({
header,
subHeader,
title,
text,
horizontalVariant,
customization: {
headerTag,
headerColor,
subHeaderTag,
subHeaderColor,
titleTag,
titleColor,
textTag,
textColor
},
popoverOnClick,
}: Slide): React.ReactElement => {
const margin = horizontalVariant ? 20 : 0;
return (
<Box>
<Element
<Element>
<Element
Component={headerTag}>
{header}
</Element>
<Element>
<Element
Component={subHeaderTag}>
{subHeader}
</Element>
</Element>
</Element>
<Element>
<Element
Component={titleTag}>
{title}
</Element>
{text && (
<Element
Component={textTag}>
<Element Component="span" theme={{ color: textColor }}>
{text}
</Element>
{!tooltip && popoverOnClick && (
<span className={styles.popover}>
<ClickableIcon
directory="dls"
imageName="dls-glyph-info"
theme={{ color: Color.BrightBlue }}
$size={Size.SM}
onClick={popoverOnClick}
/>
</span>
)}
</Element>
)}
</Element>
</Element>
</Box>
);
};
const Scroller = ({
data: { slides },
showFloatingBanner,
customization,
$pos,
}: Props): React.ReactElement => {
const customizations = { ...defaultCustomization, ...customization };
const Slides = () => {
const head = showFloatingBanner && slides[0] ? slides[0] : null;
const floatingBanner = head ? (
<Element>
<SingleSlide
key={`slide-${head.key || head.title}`}
header={head.header}
subHeader={head.subHeader}
title={head.title}
text={head.text}
horizontalVariant
customization={customizations}
popoverOnClick={head.popoverOnClick}
/>
</Element>
) : null;
const remainder = showFloatingBanner ? slides.slice(1) : slides;
return (
<>
{floatingBanner}
<AccelerationFilm $pos={$pos}>
{remainder.map(
({
key,
header,
subHeader,
title,
text,
popoverOnClick,
}) => (
<SingleSlide
key={`slide-${key || title}`}
header={header}
subHeader={subHeader}
title={title}
text={text}
customization={customizations}
popoverOnClick={popoverOnClick}
/>
)
)}
</AccelerationFilm>
</>
);
};
return (
<div>
<Breakpoint>
{({ sm }) =>
sm ? (
<Box>
{slides.map(
({
key,
header,
subHeader,
title,
text,
popoverOnClick,
tooltip
}) => (
null
)
)}
</Box>
) : (
<Slides />
)
}
</Breakpoint>
</div>
);
};
Scroller.defaultProps = {
$pos: 0,
customization: defaultCustomization,
};
export default Scroller;
This is the bit with the scrolling logic.
import { Tools } from "components-core/src/types";
import React, { useState } from "react";
import classnames from "classnames";
import Element from "components-core/src/Element";
import Icon from "components-atom-icon/src";
import withEventTracking from "components-utilities/src/withEventTracking";
import { Size } from "components-core/src/enums";
import styles from "./index.less";
interface FlipperProps {
mode: string;
children: React.ReactElement;
onClick?: () => void;
}
interface AccelerationFilmProps {
children: React.ReactElement[];
$pos?: number;
}
const ControlWithTracking = withEventTracking<FlipperProps>(
({ mode, children, onClick, qeId }: FlipperProps) => (
<Element
type="button"
onClick={onClick}
className={classnames(
styles.arrow,
mode === "left" ? styles.arrowLeft : styles.arrowRight
)}
>
{children}
</Element>
)
);
export const AccelerationFilm = ({
children,
qeId,
$pos
}: AccelerationFilmProps): React.ReactElement => {
const slideCount = React.Children.count(children);
const [position, setPosition] = useState(Math.max(0, Math.min($pos, slideCount - 3)));
return (
<Element>
{slideCount > 3 && position !== 0 && (
<ControlWithTracking
mode="left"
onClick={() => setPosition(position - 1)}
>
<Icon
/>
</ControlWithTracking>
)}
<ul
className={classnames(styles.list, {
[styles.listFull]: slideCount > 3
})}
style={{
transform: `translateX(-${position * 33.3333}%)`,
transition: `transform 500ms ease-in-out`
}}
>
{children.map((item, i) => {
return (
<Element
key={`accelerator-film-${i}`}
>
{item}
</Element>
);
})}
</ul>
{React.Children.count(children) > 3 &&
position < React.Children.count(children) - 3 && (
<ControlWithTracking
mode="right"
onClick={() => setPosition(position 1)}
>
<Icon
/>
</ControlWithTracking>
)}
</Element>
);
};
export default AccelerationFilm;
Clickable Icon component inside Scroller component.
import styles from "./index.less";
interface ClickableProps {
onClick?: (event: MouseEvent<HTMLElement>) => void;
children?: React.ReactNode;
}
const ClickableIcon = ({
onClick,
...props
}: ClickableProps) => {
return (
<Element
type="button"
onClick={onClick}
>
<Icon
/>
</Element>
);
};
export default ClickableIcon;
CodePudding user response:
You are calling getData on every render cycle and thus causing a reset of the state:
data={getData(allData)}
The solution will be to wrap the getData function with a useCallback hook:
const getData = useCallback((content) => content.map((c, i) => ({
header: c.header,
customOnClick: c.customOnClick && (() => {
setClicked(true); // this is whats causing the reset for scroll
}),
}
)),[]);
CodePudding user response:
Explanation:
Each time setClick
is called, the value of isClicked
is changed, which causes MyComponent
to be reevaluated. Since allData
is initialized inside MyComponent
, it will be reinitialized each time MyComponent
is reevaluated. Another issue is that the data
being sent to Scroller
is the result of a function that takes in allData
. Each time MyComponent
is reevaluated, that function will run again and return a new array instance given the new allData
instance. This means that every time MyComponent
reevaluates, Scrollbar
gets a new instance of data
, causing anything that consumes data
inside of Scrollbar
to also be reevaluated.
Solution:
My suggestion would be to utilize react's useMemo
hook (docs: https://reactjs.org/docs/hooks-reference.html#usememo) to 'memoize' the data going into Scroller
:
import React from 'react';
import Scroller from 'comp-external-lib';
import PopUpComponent from './PopUpComponent';
const MyComponent = ({data}) => {
const [isClicked, setClicked] = React.useState(false);
const scrollerData = React.useMemo(()=> {
return data.allData.map((c, i) => ({
header: c.header,
customOnClick: c.customOnClick && (() => {
setClicked(true); // this is whats causing the reset for scroll
}),
}
));
},[data])
return (
<>
<Scroller
data={scrollerData}
/>
{
{/* Doesn't matter if this is commented out. The scrolling will still reset due to call to setClicked function */}
{/* isClicked && <PopUpComponent /> */}
}
</>
);
};
export default MyComponent;
Also fun fact, <>
is shorthand for React's Fragment
CodePudding user response:
The problem could be that each time you click the component your Scroller
gets a different reference of the data and because of that, it calls lifecycle methods that cause your performance issue.
If you will send the same props ( same reference ) to Scroller
it should not call any lifecycle method which propably causes your problems.
import React, { Fragment, useMemo, useState } from 'react'
import Scroller from 'comp-external-lib'
import PopUpComponent from './PopUpComponent'
const MyComponent = props => {
const [isClicked, setClicked] = useState(false)
const { allData } = props.data
const getData = content =>
content.map((c, i) => ({
header: c.header,
customOnClick:
c.customOnClick &&
(() => {
setClicked(true)
})
}))
const scrollerData = useMemo(() => getData(allData), [allData])
return (
<Fragment>
<Scroller data={scrollerData} />
{isClicked && <PopUpComponent />}
</Fragment>
)
}
export default MyComponent