React-virtualized list running below 60fps. How to optimize? - reactjs

I am trying to create a post feed like the one instagram has (on the main page).
I'm using Infinite-loader for fetching, Window-scroller for using the window as the scroll, auto-sizer for sizing the list how i want and CellMeasurer for measuring the 'post component' once after the image has been loaded.
Here is code for the list component:
class PostsPartial extends React.PureComponent<IProps>{
state: IPostsPartialState = { posts: [], hasMorePosts: true }
private cache: CellMeasurerCache;
private get rowCount(): number {
return this.state.hasMorePosts ? this.state.posts.length + 1 : this.state.posts.length;
}
constructor(props: IProps) {
super(props);
this.cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 1000
});
this.renderRow = this.renderRow.bind(this);
}
private fetchPosts = ({ startIndex, stopIndex }: { startIndex: number, stopIndex: number }) => {
return getNewPostsChunk(startIndex, stopIndex - startIndex, this.props.token).then((res: IPostsChunkResponse) => {
if (res.success) {
if (res.posts.length === 0) {
// no more posts
this.setState({ hasMorePosts: false })
}
else {
let newPosts = [...this.state.posts, ...res.posts];
this.setState({ posts: newPosts })
}
}
else {
// internal error
}
})
};
private renderRow({ index, key, parent, style }: any) {
return (
<CellMeasurer
cache={this.cache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
{({ measure, registerChild }: any) => (
<div className={styles.paddingContainer} ref={registerChild} style={style}>
<Post
isLoaded={this.isRowLoaded({index})}
measure={measure}
post={this.state.posts[index]}
/>
</div>
)}
</CellMeasurer>
);
}
private isRowLoaded = ({ index }: { index: number }) => {
return !!this.state.posts[index];
};
public render() {
return (
<div className={styles.mainContainer}>
<InfiniteLoader
isRowLoaded={this.isRowLoaded}
loadMoreRows={this.fetchPosts}
rowCount={this.rowCount}
>
{({ onRowsRendered, registerChild }: InfiniteLoaderChildProps) => (
<WindowScroller>
{({ height, isScrolling, onChildScroll, scrollTop }) => (
<AutoSizer disableHeight>
{
({ width }: any) => (
<List
ref={registerChild}
onRowsRendered={onRowsRendered}
autoHeight
width={width}
height={height}
isScrolling={isScrolling}
onScroll={onChildScroll}
scrollTop={scrollTop}
deferredMeasurementCache={this.cache}
rowHeight={this.cache.rowHeight}
rowRenderer={this.renderRow}
rowCount={this.rowCount}
overscanRowCount={10}
/>
)
}
</AutoSizer>
)}
</WindowScroller>
)}
</InfiniteLoader>
</div>
);
}
and here is code for the post component:
const Post:React.FC<IProps> = (props:IProps) => {
if(props.post && props.isLoaded)
return (
<div className={styles.container}>
<Segment className={styles.profileSegmentInternal} attached='top'>
<Image className={styles.verySmallImg} circular size='tiny' src={`${settings.BASE_URL}/feed/photo/user/${props.post.creator}`}></Image>
<Link to={`/profile/${props.post.creator}`}>
<Header size='small' className={styles.headerName} as='span'>{props.post.creator}</Header>
</Link>
</Segment>
<div className={styles.imageContainer}>
<Image onLoad={props.measure} src={`${settings.BASE_URL}/feed/photo/post/${props.post._id}`} className={styles.image}></Image>
</div>
<Segment className={styles.bottomSegment} attached='bottom'>
<>
<Menu className={styles.postMenu}>
<Item className='left'>
<Icon className={styles.iconBtn} size='big' name='heart outline'></Icon>
<Icon className={styles.iconBtn} size='big' name='comment outline'></Icon>
<Icon className={styles.iconBtn} size='big' name='paper plane outline'></Icon>
</Item>
<Item className='right'>
<Icon className={styles.iconBtn} size='big' name='bookmark outline'></Icon>
</Item>
</Menu>
</>
<Header className={styles.likes} size='tiny'>{props.post.likesCount} likes</Header>
<Header className={styles.description} size='tiny'>
<Header size='tiny' className={styles.commentUsername} as='span'>{props.post.creator}</Header>
<Header className={styles.commentText} as='span' size='tiny'> {props.post.description}</Header>
</Header>
<Link to='#'>
<Header className={styles.viewAllComments} size='tiny' disabled>View all comments</Header>
</Link>
{
//backend will return the first 3-4 messeges only
// props.post.messeges.map((messege,index) => (
// ))
}
<Form className={styles.commentForm}>
<Form.Field className={styles.commentField}>
<Form.Input
className={styles.commentInput}
placeholder='Adding comment ...'
>
</Form.Input>
<Button className={styles.commentSubmit} size='medium' primary>Comment</Button>
</Form.Field>
</Form>
</Segment>
</div>
)
else
return (
<p>loading</p>
)
Even if I remove everything from the post component and leave only the image, it still won't run with more then 45-50fps sometimes going under 40fps too.
Can I optimize my approach in any way or am I doing something wrong?
Should I provide anything else that might be helpful?
Thank you in advance!

So I fixed my problem by resizing the image when uploading it (in the backend using sharp).
This way the fetching is faster witch makes the (just in time) measure of a post component way faster as less data needs to be loaded on mount, and html+css doesn't have to resize the high image into a smaller container.
Sounds silly, but I didn't think of this being the issue and instead focused on my infinite scrolling implementation :D
Ya live and ya learn
EDIT:
I forgot to mention, a small change I did when setting the image src.
Instead of making an express route that retrieves the src, I can just use it while receiving the post info. The file source won't be printed with console.log or whatever, but it is there and can be used like so:
<Image className={styles.image} onLoad={props.measure} src={`data:${props.post.source.contentType};base64,${Buffer.from(props.post.source.data).toString('base64')}`} />

Related

react-beautiful-dnd draggable flickering (but only some of the draggables children)

I am having trouble getting react-beautiful-dnd to work because children of the draggable are flickering after reordering:
No external requests happen in the background, the app stores the change only in the local state (see first code block reorderElements()).
What really confuses me is how only the 2 checkboxes flicker, but not the text or the icon.
The list rerenders only once after reorder (asynchronously when the state is updated)
I suspect the asynchronous nature of useState to be the issue. But the fact that only those 2 checkboxes flicker contradicts the suspicion in my opinion.
The draggableId is updated after each reorder to the index of each element.
Here is the code:
My reorder function that is called onDragEnd:
function reorderElements({ source, destination }: any) {
if (!destination) {
return;
}
setAppState((oldAppState: AppState | null) => {
if (oldAppState) {
const { content } = oldAppState;
const copy = JSON.parse(JSON.stringify(content));
const [elementToMove] = copy.splice(source.index, 1);
copy.splice(destination.index, 0, elementToMove);
const elementsWithNewIndex = copy.map(
(e: BannerElement, i: number) => ({
...e,
id: "" + i,
})
);
return { ...oldAppState, content: elementsWithNewIndex };
}
return null;
});
}
The list component
<DragDropContextContainer onDragEnd={onDragEnd}>
<DroppableContainer direction="horizontal" droppableId="root">
{(provided: DroppableProvided) => (
<div
style={{ display: "flex" }}
ref={provided.innerRef}
{...provided.droppableProps}
className="draggable-container-custom"
>
<>
{content.map((element, index) => (
<Element
key={index}
index={index}
element={element}
updateElement={updateElement}
deleteElement={deleteElement}
provided={provided}
nElements={nElements}
/>
))}
{provided.placeholder}
</>
</div>
)}
</DroppableContainer>
</DragDropContextContainer>
DragDropContextContainer
export function DragDropContextContainer({ children, ...props }: Props) {
return <DragDropContext {...props}>{children}</DragDropContext>;
}
DroppableContainer
export function DroppableContainer({ children, ...props }: Props) {
return <Droppable {...props}>{children}</Droppable>;
}
Each Element
<DraggableContainer draggableId={id} index={index} key={index}>
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => {
const draggableStyle = snapshot.isDragging
? { ...customDraggableStyle, ...provided.draggableProps.style }
: provided.draggableProps.style;
return (
<div
ref={provided.innerRef}
{...provided.draggableProps}
style={draggableStyle}
className="draggable-custom"
>
<div
{...provided.dragHandleProps}
className="element-drag-icon-container"
>
<div className="element-drag-icon">
<PolarisIcon source={DragHandleMinor} color={"base"} />
</div>
</div>
<div className="element-container">
<div className="element-icon-container">
<Checkbox
label="Text has Icon Prefix"
checked={hasIcon}
onChange={(newValue) => {
updateElement({ id, hasIcon: newValue });
}}
/>
<Icon
disabled={!hasIcon}
key={JSON.stringify(icon)}
id={id}
elementIcon={icon}
updateElement={updateElement}
/>
</div>
<div className="element-text-container">
<TextField
label="Set your Text:"
multiline={3}
value={text}
onChange={(text: ElementText) => {
updateElement({ id, text });
}}
autoComplete="off"
/>
</div>
<div className="element-bottom-row">
<Checkbox
label="Show on mobile"
checked={showMobile}
onChange={() => {
updateElement({ id, showMobile: true });
}}
/>
<Button
plain
destructive
onClick={() => {
deleteElement(id);
}}
>
delete
</Button>
</div>
</div>
</div>
);
}}
</DraggableContainer>
);
DraggableContainer
export function DraggableContainer({ children, ...props }: Props) {
return <Draggable {...props}>{children}</Draggable>;
}
Those container-Components exist to get rid of some ESLint errors.
Console logs nothing regarding this package.
im using "react": "^17.0.2" and "react-beautiful-dnd": "^13.1.0"
tested in Chrome Version 108.0.5359.124 (Official Build) (arm64)

How to store original data while updating state react

im working on a feedback app,and im updating an state by filtering it, so i can click on a tag and display the feedbacks filtered by tags, im passing the values through context
const handleFiltering = (category) => {
const filteredData = cardInfo?.filter((item: feedbackTypes) => item.categories === category);
setCardInfo(filteredData);
};
return <DataContext.Provider value={{ cardInfo }}>{children}</DataContext.Provider>;
and these are, the card component (where im rendering the feedbacks)
const NewTable: FC = (): JSX.Element => {
const { cardInfo, setCardInfo } = useContext(DataContext);
return (
<>
<Stack spacing={6} mt={4} ml="38.2vw">
{cardInfo ? (
cardInfo?.map((values: feedbackTypes) => (
<div key={values._id}>
<Link
to={{
pathname: '/editFeedback',
state: {
id: values._id,
title: values.title,
feedback: values.text,
category: values.categories
}
}}
>
<Card
title={values.title}
feedback={values.text}
cardId={values._id}
category={values.categories} />
</Link>
</div>
))
) : (
<EmptyCard />
)}
</Stack>
</>
);
};
and the components where the tags are rendered
const NavBar = () => {
const { cardInfo, handleFiltering, permanentData } = useContext(DataContext);
return (
<>
<Flex direction="row">
<Box borderRadius="10px" className={styles.leftCard}>
<Heading fontSize="xl" color="white" mt={14} ml={6}>
Frontend Mentor
</Heading>
<Text mt={2} ml={6}>
Feedback Board
</Text>
</Box>
<Box borderRadius="10px" className={styles.leftCardSort}>
<Heading fontSize="xl" color="white" mt={10} ml={6}></Heading>
<Text ml={4} className={styles.tagSort}>
{permanentData?.map((values, idx) => (
<div key={idx}>
<Tag
className={selectedTag?.includes(values as never) ? styles.tagSelected : styles.tagUnselected}
onClick={(e) => {
handleFiltering(values);
handleBtnStyle(values as never);
}}
>
{values}
</Tag>
</div>
))}
</Text>
</Box>
whenever i click on each tag, the feedbacks that contain those tags are filtered correctly, but when i click on another tag after selecting one, it does not render the other feedbacks until i click on 'All' (and thats because im fetching all the data again), i dont know how could i re render other feedbacks after i already clicked on one, because the feedback component needs to be filtered just like the tag is filtering them by categories, i dont know what logic to follow from here on
first tag selected
second tag selected after the first

How can I get elements from one component in another one by id or value?

I've created a component to create follow and unfollow buttons and now I want to use this component in other components (like Suggestions).
In the suggestions component I want to show only the button that its value is equal to the user.id, but I am only able to get the 5 buttons from the original component.
Is there a way to select only the button that is equal to the user.id?
This is the component that creates the buttons:
render() {
const { users, followingUsers } = this.state
const userId = this.props.user[0].id
return(
<div>
{users.map((user, index) => {
if(userId !== user.id) {
if(followingUsers.includes(user.user_name)) {
return(
<Button key={index} value={user.id} onClick={this.onUnfollow}>Unfollow</Button>
)
} else {
return(
<Button key={index} value={user.id} onClick={this.onFollow}>Follow</Button>
)
}
}
})}
</div>
)
}
}
export default withUser(Unfollowfollow);
Here is the suggestions component:
render() {
const { users } = this.state
const userId = this.props.user[0].id
return (
<div>
<ul>
{users.map((user, index) => {
if(user.id !== userId) {
return (
<Card className="users" key= {index}>
<CardBody>
<CardImg className="picfollowers" top width="9%" src={user.image} />
<CardTitle onClick={() => this.handleClick(user.id)}>{user.user_name}</CardTitle>
<Unfollowfollow />
</CardBody>
</Card>
)}
})}
</ul>
</div>
)
}
}
export default withUser(Suggestions);

Remove previous content when new one is rendered through a condition (React)

I have a navbar that uses eventKeys to switch between the buttons
const CustomNav = ({ active, onSelect, ...props }) => {
return (
<Nav
{...props}
activeKey={active}
onSelect={onSelect}
style={{ marginBottom: "15px" }}>
<Nav.Item eventKey='all'>All</Nav.Item>
<Nav.Item eventKey='movies'>Movies</Nav.Item>
<Nav.Item eventKey='shows'>Shows</Nav.Item>
<Nav.Item eventKey='people'>People</Nav.Item>
</Nav>
);
};
I did this:
const Content = () => {
if (this.state.active === "all") {
return (
<div>
{trending.results &&
trending.results.map((i) => (
<React.Fragment key={i.id}>
<p>{i.title}</p>
</React.Fragment>
))}
</div>
);
} else if (this.state.active === "movies") {
return (
<div>
{trendingMovies.results &&
trendingMovies.results.map((i) => (
<React.Fragment key={i.id}>
<p>{i.title}</p>
</React.Fragment>
))}
</div>
);
}
};
Called it here:
return (
<div className='Home'>
<FlexboxGrid justify='center'>
<Panel bordered header='Trending today!'>
<CustomNav
className='customNav'
appearance='subtle'
active={active}
onSelect={this.handleType}
/>
<Content />
<Pagination
{...this.state}
style={{ marginTop: "15px" }}
maxButtons={5}
size='sm'
pages={totalPages}
activePage={this.state.activePage}
onSelect={this.handlePage}
/>
</Panel>
</FlexboxGrid>
</div>
);
}
}
To display the correct data for each tab, but when I'm on the movies tab it shows all the data from the first "all" tab + data on the "movies" tab. I wanna show each data individually corresponding to the correct tab which is controlled by "this.state.active". Tried a switch statement too and that did not work
you are using the arrow syntax
const Content = () => { ... }
and also using this.state variable in your code !!!
if you want to use this.state, then you want to use the class syntax, like
class Content extends React.Component { ... }
but don't mix the two styles.
what you are probably wanting to do is to send the active variable as a prop
try:
const Content = ({active}) => {
if (active === 'all') {
return (...)
} else if (active === 'movies') {
return (...)
}
return null
}
and where you are calling the component you send the active value in as a prop
<Content active={active} />
Note also that you are using the variables trending and trendingMovies and it is unclear where those come from, you may need to send those via props also.
Now you can also leave the if..else logic outside of your Content component like so
const Content = ({myTrending}) => {
return (
<div>
{myTrending.results &&
myTrending.results.map((i) => (
<React.Fragment key={i.id}>
<p>{i.title}</p>
</React.Fragment>
))}
</div>
);
}
and then where you call that component you have
<Content
myTrending={active === 'all' ? trending : trendingMovies}
/>
You need to pass active and other variables as props to the Content component, since it doesn't access them otherwise:
const Content = ({active, trending=[], trendingMovies=[]}) => {
if (active === "all") {
return (
<div>
{trending.results.map((i) => (
<React.Fragment key={i.id}>
<p>{i.title}</p>
</React.Fragment>
))}
</div>
);
} else if (active === "movies") {
return (
<div>
{trendingMovies.results.map((i) => (
<React.Fragment key={i.id}>
<p>{i.title}</p>
</React.Fragment>
))}
</div>
);
}
};
return (
<div className='Home'>
<FlexboxGrid justify='center'>
<Panel bordered header='Trending today!'>
<CustomNav
className='customNav'
appearance='subtle'
active={active}
onSelect={this.handleType}
/>
<Content active={this.state.active} trending={this.state.trending} trendingMovies={this.state.trendingMovies} />
<Pagination
{...this.state}
style={{ marginTop: "15px" }}
maxButtons={5}
size='sm'
pages={totalPages}
activePage={this.state.activePage}
onSelect={this.handlePage}
/>
</Panel>
</FlexboxGrid>
</div>
);
}
}

React Virtualized Masonry does not resize with the browser

I am trying to build a feed (a Pinterest-like feed to put it straight). I am using react-virtualized Masonry component.
You can see how the items rearrange and the component is correctly resized when the browser window resizes in this screen recording.
However, mine has a strange behavior as you can see in this screen recording.
Here's the relevant excerpt of my code:
export default class Feed extends Component <PropTypes, State> {
static readonly defaultProps = {
enableInfiniteScroll: false,
chunkSize: 9,
};
private _windowScroller: WindowScroller;
private _masonry: Masonry;
private _columnCount: number;
private _cache: CellMeasurerCache;
private _cellPositioner: Positioner;
constructor(props: PropTypes) {
super(props);
// ...
this._columnCount = 3;
this._cache = new CellMeasurerCache({
defaultWidth: COLUMN_WIDTH,
defaultHeight: 400,
fixedWidth: true,
});
this._cellPositioner = createMasonryCellPositioner({
cellMeasurerCache: this._cache,
columnCount: this._columnCount,
columnWidth: COLUMN_WIDTH,
spacer: GUTTER,
});
}
onResize({width}: Size) {
this._cache.clearAll();
this.calculateColumnCount(width);
this.resetCellPositioner();
this._masonry.recomputeCellPositions();
}
cellRenderer(cellProps: MasonryCellProps) {
const {items} = this.state;
const listing = items.get(cellProps.index);
return (
<CellMeasurer
cache={this._cache}
index={cellProps.index}
key={cellProps.key}
parent={cellProps.parent}
>
<div style={cellProps.style}>
<ListingCard company={listing} />
</div>
</CellMeasurer>
);
}
calculateColumnCount(width: number) {
this._columnCount = Math.floor((width + GUTTER) / (COLUMN_WIDTH + GUTTER));
}
resetCellPositioner() {
this._cellPositioner.reset({
columnCount: this._columnCount,
columnWidth: COLUMN_WIDTH,
spacer: GUTTER,
});
}
render() {
const {items, isLoading, hasMore} = this.state;
return (
<div className={Styles['listings-feed']}>
<WindowScroller scrollElement={window} ref={this.setRef}>
{({height, isScrolling, onChildScroll, scrollTop, registerChild}) => (
<div className={Styles.windowScrollerContainer}>
<AutoSizer disableHeight onResize={this.onResize}>
{({width}) => (
<div ref={registerChild as any}>
<Masonry
cellCount={items.size}
cellMeasurerCache={this._cache}
cellPositioner={this._cellPositioner}
cellRenderer={this.cellRenderer}
height={height}
width={width}
autoHeight
ref={(r: Masonry) => this._masonry = r}
/>
</div>
)}
</AutoSizer>
</div>
)}
</WindowScroller>
</div>
);
}
}
After testing with different parameters and tweaks, I found out is was not rendering all of the items because they were technically out of range (not in the user's view). They were not out of view when scrolling, it is just that the <Masonry /> component only updates on property changes.
Since I am using a <WindowScroller /> component, I found out it offers a scrollTop variable for the children function so I passed this directly to the Masonry component:
<WindowScroller scrollElement={window} ref={this.setRef}>
{({height, isScrolling, onChildScroll, scrollTop, registerChild}) => (
<div className={Styles.windowScrollerContainer}>
<AutoSizer disableHeight onResize={this.onResize}>
{({width}) => (
<div ref={registerChild as any}>
<Masonry
cellCount={items.size}
cellMeasurerCache={this._cache}
cellPositioner={this._cellPositioner}
cellRenderer={this.cellRenderer}
height={height}
width={width}
autoHeight
ref={(r: Masonry) => this._masonry = r}
scrollTop={scrollTop}
/>
</div>
)}
</AutoSizer>
</div>
)}
</WindowScroller>

Resources