React Memory Leaks Caused by Intersection Observer - javascript

Background Info: My React app was crashing on mobile. Chrome dev tools indicated garbage collection wasn't triggering. Looking into the heap the top constructors by retained size were all referencing intersection observor (used for infinite scrolling in many of my react components).
Question: How can I go about fixing the memory leaks caused by intersection observor? Is there a way to trigger garbage collection when the components unmount?
Sample Component using Intersection Observor for Infinite Scroll:
import React, { useEffect, useState, useRef, useCallback } from 'react'
import { Link } from 'react-router-dom'
import { uuid, fabricateTimeStamp, getRandom } from '../containers/helperFunctions'
import { avatarQuery } from '../words'
import quote from 'inspirational-quotes'
import history from '../history'
import { thumbsUp, thumbsDown, arrowDrop } from './svgs'
import {
fetchAvatars as callAvatarsAPI,
fetchVideos as callVideosAPI } from '../containers/api'
const ActivityFeed = (props) => {
const [firstRenderDone, setFirstRenderDone] = useState()
const [comments, setComments] = useState([])
useEffect(() => {
const mobile = window.innerWidth <= 600
if (mobile) fetchAvatars('woman', 3)
if (!mobile) fetchAvatars('woman', 6)
if (history.location.pathname.includes('/video/') || history.location.pathname.includes('/search/')) {
document.querySelector('.activityFeedContainer').classList.toggle('hide')
}
}, [])
// if user clicks nav button, remove all comments generated by infinite scroll
useEffect(() => {
setComments(prevState => (prevState.slice(0, 8)))
}, [props.button])
// INFINITE SCROLL
// Callback is triggered when ref is set in mapCommentsToHTML
const observer = useRef()
const lastActivityPost = useCallback(lastPostNode => {
observer.current = new IntersectionObserver(entries => {
const lastPost = entries[0]
if (lastPost.isIntersecting) fetchAvatars(getRandom(avatarQuery), 6)
})
if (lastPostNode) observer.current.observe(lastPostNode)
})
const fetchAvatars = async (query, amount) => {
let response = await callAvatarsAPI(query, amount)
response = response.data.hits
mapCommentsToHTML(response)
}
const mapCommentsToHTML = (response) => {
const picsMappedToHTML = response.map((pic, index) => {
return (
<div className="commentWrapper" key={uuid()} ref={response.length === index + 1 ? lastActivityPost : null}>
<div className="avatarPlaceholder--comments">
{props.page === 'channel'
? <img
className="avatarPlaceholder--img"
src={
props.userAvatar.previewURL ? props.userAvatar.previewURL
: props.userAvatar.userImageURL === "" ? 'https://i.imgur.com/ZwDgXSF.jpg'
: props.userAvatar.userImageURL}
alt="An Activity Feed User Avatar" />
: <Link to={`/channel/${pic.id}`}>
<img
className="avatarPlaceholder--img"
src={pic.previewURL}
alt="An Activity Feed User Avatar" />
</Link>
}
</div>
<div className="commentContainer" >
<Link to={`/channel/${pic.id}`}>
<h5 className="commentorName">{props.page === 'channel' ? props.userName : pic.user}</h5>
</Link>
<span className="dateOfComment">{fabricateTimeStamp(index)}</span>
<p className={`${props.page}-comment`}>{quote.getQuote().text}</p>
<div className="thumbs">
<span className="thumbsUpIcon">
{thumbsUp(16)}
</span>
<span className="thumbsDownIcon">
{thumbsDown(16)}
</span>
</div>
<p className="replyText">REPLY</p>
<div className="viewReplies">
<span className="arrowDrop">
{arrowDrop()}
</span>
View {Math.floor(Math.random() * 50) + 2} Replies
</div>
</div>
</div>
)
})
setComments(prevState => ([...prevState, ...picsMappedToHTML]))
}
return (
<aside className="activityFeedContainer">
<h1 className={`${props.page}--activity-feed-title`}>Activity Feed</h1>
<hr className="home--activityfeed-hr" />
<div className="commentSection--activityfeed">
{comments}
</div>
</aside>
)
}
export default ActivityFeed
Chrome Performance Timeline (dip # 27 secs is due to clicking an <a> tag which triggered page refresh)
Heap Snapshots:

I believe you need to stop observing at some point , you might want to
call disconnect on your IntersectionObserver when you unmount

Related

Next.js getStaticProps not refreshing the data [duplicate]

This question already has an answer here:
How to add new pages without rebuilding an app with +150k static pages?
(1 answer)
Closed 11 months ago.
I've jus started working and learning Next so I have a lot of confusions,
I was using the useEffect on react and it always updated the UI with the new stuff that was added to the API however, its not working on next.js
SO I have an index file
import Link from "next/link";
import react, {useState, useEffect} from "react";
import { useRouter } from 'next/router';
export async function getStaticProps({ res }) {
try {
const result = await fetch(`https://api.pandascore.co/matches/running??sort=&page=1&per_page=10&&token=#`);
const data = await result.json();
return {
props: { game: data },
revalidate: 10 // 10 seconds
};
} catch (error) {
res.statusCode = 404;
return { props: {} };
}
}
const upcomingGames = ({ game }) => {
return (
<div className="container">
<h2>Live Games - </h2>
<div className="columns is-multiline">
{game.map(q => (
<div className="column is-half" key={q.id}>
<div className="inner">
<div className="inner__box">
<Link href = {'/live/' + q.slug} key={q.slug}>
<a className="h2link" key={q.slug}> {q.name}</a>
</Link></div>
</div>
</div>
))}
</div>
</div>
);
}
export default upcomingGames;
This file is connected to a [slug].js file which displays more details about a game,
Now in production when I deployed the app to vercel I have noticed that when a new game is added to the API it displays in the index.js but when I click on it I'm redirected to a fallback(404) page.
After I redeploy my project this is fixed, however every time a new game is added and rendered I'm unable to access its individual page which I defined in [slug].js
export const getStaticPaths = async () => {
const res = await fetch(`https://api.pandascore.co/matches/running?sort=&page=1&per_page=50&token=#`);
const data = await res.json();
const paths = data.map(o => {
return {
params: { slug: o.slug.toString() }
}
})
return {
paths,
fallback: false
}
}
export const getStaticProps = async (context) => {
const slug = context.params.slug;
const res = await fetch(`https://api.pandascore.co/matches/running?search[slug]=${slug}&token=#`);
const data = await res.json();
console.log(data)
return {
props: {
game: data
}
}
}
export default function live({ game }) {
return (
<div className="container">
<h2> Single Game deets.</h2>
{game.map((g) => (
<div className="container" key={g.id} >
<div className="inner-box" key={g.slug}>
{/** Fetch team and display their corresponding score - A bit of code repition :( */}
<div className="score-board-min columns is-mobile is-multiline">
<div className="column is-full"> {g.opponents.slice(0, -1).map((o) => <span className="team" key={o.id}>{o.opponent.name}</span>)}
{g.results.slice(0, -1).map((res, i) => (
<span className="scores" key={i}>{res.score}</span>
))}</div>
<div className="column">
{g.opponents.slice(-1).map((o) => <span className="team" key={o.id}>{o.opponent.name}</span>)}
{g.results.slice(-1).map((res, i) => (
<span className="scores" key={i}><div>{res.score}</div></span>
))}
</div>
</div>
<br />
<div className="lower-box columns is-multine">
<div className="column is-half">
<div className="dark"><span className="is-pulled-left">League</span> <span className="is-pulled-right">{g.league && g.league.name}</span></div>
<div className="dark"><span className="is-pulled-left">Game:</span> <span className="is-pulled-right"> {g.videogame && g.videogame.name} </span></div>
<div className="dark alt"><span className="is-pulled-left">Tournament</span> <span className="is-pulled-right"> {g.tournament && g.tournament.name} | </span></div>
<div className="dark"><span className="is-pulled-left">Series</span> <span className="is-pulled-right"> {g.serie.full_name} | {g.serie.tier.toUpperCase()} </span></div>
<div className="dark alt"><span className="is-pulled-left">Teams</span> <span className="is-pulled-right"> {g.opponents.map((o) => o.opponent.name).join(" vs ")} </span></div>
</div>
</div>
<br />
</div>
</div>
))}
</div>
)
}
During development (next dev) getStaticPaths gets called on every request, but for production it only gets called the next time you run next build. So when a new game is added to the API, the paths named after ${some_new_game_slug} won't exist until you run next build again, i.e., re-deploy. If this type of data changes frequently, you might have to use getServerSideProps for [slug].js as well (so no static paths) or opt for the client-side data fetching approach.

How to change pagination variables in react

I am working on building pagination. I'm still working on my API to fetch posts based on pagination, but at the moment I am stuck on a problem with states.
In my main file (where the posts will be), my code looks like this:
// Import modules
import React from "react";
import axios from "axios";
import url from "url";
// Import components
import { Snippet } from "../Snippet";
import { CreatePost } from "./CreatePost";
import { Pagination } from "./Pagination";
// Import styles
import "./css/Content.css";
// Axios Init
const api = axios.create({
baseURL: `http://localhost:5000/api/`,
});
export class Content extends React.Component {
state = {
collections: [
{ title: "ReactJS", color: "red" },
{ title: "HTML", color: "cyan" },
{ title: "CSS", color: "pink" },
],
snippets: [],
limitSnippets: 3,
page: 0,
};
constructor(props) {
super(props);
}
componentDidMount() {
this.getSnippets();
}
getSnippets = async () => {
try {
let data = await api
.get(
`/snippets/fetchAll?limitSnippets=${this.state.limitSnippets}&page=${this.state.page}`,
{
body: {
limitSnippets: 3,
page: 1,
},
}
)
.then(({ data }) => data);
this.setState({ snippets: data });
} catch (error) {
console.log(error);
}
};
updatePagination = (page) => {
this.state.page = page;
console.log(this.state.page);
};
render() {
return (
<div className="content">
<h1 className="content-header">Snippets</h1>
<CreatePost contentUpdater={this.getSnippets} />
<Pagination updatePagination={this.updatePagination} />
<div className="w-layout-grid grid">
{this.state.snippets.map((snippet, i) => {
return (
<Snippet
key={i}
title={snippet.title}
code={snippet.code}
updatedDate={snippet.updatedDate}
createdDate={snippet.createdDate}
language={snippet.language}
creator={snippet.creator}
collections={snippet.collections}
/>
);
})}
</div>
<Pagination />
</div>
);
}
}
export default Content;
In pagination file, my code looks like this:
export const Pagination = (props) => {
// States
const [page, setPage] = useState(0);
// Axios Init
const api = axios.create({
baseURL: `http://localhost:5000/api/`,
});
const handleLeft = (event) => {
event.preventDefault();
if (page > 0) {
setPage(page - 1);
props.updatePagination(page);
} else {
console.log("handleLeft(): page not > 0");
}
//props.updatePagination(page);
//}
};
const handleRight = (event) => {
event.preventDefault();
// page < fetchAllPages
setPage(page + 1);
props.updatePagination(page);
};
/*useEffect(() => {
props.updatePagination(page);
}, [page]);
*/
return (
<div className="paginate-div">
<div className="paginate-next">
<div className="paginate-next-icon" onClick={handleLeft}>
<i className="fas fa-caret-left"></i>
</div>
</div>
<a href="#" className="paginate-button first w-inline-block">
<div className="paginate-text">1</div>
</a>
<a href="#" className="paginate-button w-inline-block">
<div className="paginate-text">2</div>
</a>
<a href="#" className="paginate-button w-inline-block">
<div className="paginate-text">3</div>
</a>
<a href="#" className="paginate-button w-inline-block">
<div className="paginate-text">4</div>
</a>
<a href="#" className="paginate-button w-inline-block">
<div className="paginate-text">5</div>
</a>
<a href="#" className="paginate-button w-inline-block">
<div className="paginate-text">6</div>
</a>
<a href="#" className="paginate-button last w-inline-block">
<div className="paginate-text">...</div>
</a>
<div className="paginate-next" onClick={handleRight}>
<div className="paginate-next-icon">
<i className="fas fa-caret-right"></i>
</div>
</div>
</div>
);
};
I have my pagination component which is passed a prop that's a function to updatePagination(). The pagination component has functions for left and right button for switching thru pagination, and when it is clicked, the main file gets the pagination updated.
The problem I am having (sorry if it is confusing by the way i worded this)
The default page is 0 (which is basically page 1).
The crazy thing is when I press right (handleRight is called on submit), it stays at page 0, then if I click it again it goes to 1, then after if I press the left button (which called handleLeft on submit) while it is on page 1, it goes up to 2 somehow, but if I click it again it goes back down to 1, then if I click again it goes to 0.
Why is this strange problem occurring?
setPage is asynchronous, so when you setPage to decrement and then immediately call props.updatePage, props.updatePage is receiving the old value of page. You can read all about this common problem here.
const handleRight = (event) => {
event.preventDefault();
// Hey React, set page to page + 1 and rerender the component
setPage(page + 1);
// But before you do that, call props.updatePagination on the old value
props.updatePagination(page);
};
You should ask yourself, though, why you even store two stateful values of page at all (one in the parent component, one in the child component). Couldn't the Content component keep track of your page state (as it already does) and pass it down as a prop, getting rid of your need for a page state in the child component? This is called Lifting State Up, and it's a fundamental concept in React, which incentivizes you to use the least amount of state possible to avoid exactly this kind of desync. Furthermore, from the code you've shared, the Pagination component just displays the page numbers - why does it even need to be stateful at all?
The problem was that the old value of page was being used in updatePagination(), I fixed this by not running updatePagination(page) in the same place, I used useEffect(), and checked for changes to {page}, and ran updatePagination in there.
useEffect(() => {
props.updatePagination(page);
}, [page]);
The handleLeft, handleRight functions were changed to look like this:
const handleLeft = (event) => {
event.preventDefault();
let newPage = page - 1;
if (page > 0) {
setPage(newPage);
} else {
console.log("handleLeft(): page not > 0");
}
//}
};
NOTE"
In the comments section, someone made a point that I should not be storing the page number in two places, but rather store them in one place and pass it over as props. I have not currently tried to do this, but I plan on doing this soon. But for now, this is the best answer for this scenario.

Ternary operator not working as desired on state change

I'm trying to build a map that will show whisky distilleries as icons on the map. When one of the distillery markers is clicked, the state is updated to hold an object with that distillery data so a popup can render with relevant info. I've got a hover effect that I want to persist after clicking for as long as the state holds the obj.
I have the following ternary operator that should add 'clicked' when selectedDistillery is truthy, but the class is not applied.
className={`marker-btn ${selectedDistillery ? 'clicked' : ''}`}
The popup renders fine on click so not really sure what the issue is.
Here's the entire component
import 'mapbox-gl/dist/mapbox-gl.css';
import './App.css';
import React, { useState, useEffect } from 'react'
import ReactMapGl, { Marker, Popup } from "react-map-gl";
import * as distilleries from "./data/dist-locations.json";
const App = () => {
const [viewport, setViewport] = useState({
latitude: 56.770720743612365,
longitude: -4.2724397531559655,
width: "90vw",
height: "90vh",
zoom: 6,
});
const [selectedDistillery, setSelectedDistillery] = useState(null);
useEffect(() => {
const listener = (e) => {
if(e.key === 'Escape'){
setSelectedDistillery(null);
}
}
window.addEventListener('keydown', listener);
return () => {
window.removeEventListener('keydown', listener);
}
}, [])
console.log(selectedDistillery);
return (
<div className="main-container">
<div className="map-container">
<ReactMapGl
{...viewport}
mapboxApiAccessToken={process.env.REACT_APP_API_KEY}
onViewportChange={(viewport => { setViewport(viewport) })}
mapStyle="mapbox://styles/vdiad/ckkq0g4201s4r17peswejsg82"
>
{distilleries.features.map(distillery => {
return(
<Marker key={distillery.id} latitude={distillery.geometry.coordinates[1]} longitude={distillery.geometry.coordinates[0]} >
<button
className={`marker-btn ${selectedDistillery ? 'clicked' : ''}`}
onClick={()=>{
setSelectedDistillery(distillery);
}}
>
<img src="/barrel1.svg" alt="whisky barrel img" />
</button>
</ Marker>
)
})}
{selectedDistillery && (
<div className="popup">
<Popup
latitude={selectedDistillery.geometry.coordinates[1]}
longitude={selectedDistillery.geometry.coordinates[0]}
onClose={()=>{setSelectedDistillery(null)}}
>
<h3>{selectedDistillery.properties.NAME}</h3>
<p>Founded in: </p>
<p>Region: </p>
</ Popup>
</div>
)}
</ReactMapGl>
</div>
</div>
)
}
export default App

React rendering on load but dont update when the async calls are completed

*I'm having some issues with the components not displaying the info as the async promises are not fulfilled before the render occurs. I am sure this is just something simple, but I don't know what to google atm ;)
I am just using async-await for one api.get through axios, and the promise is fulfilled, just not before rendering of the site. I've also tried useEffect(() => {},[]); but what I aim for is that the site is loaded without any clicking with all the information ready.
I hope this makes sense! Thank you again if you have time to look at it, or leave a comment if there is anything missing.
Thank you!*
The component:
import React, { useEffect, useState } from 'react';
import getServiceDetails from '../../controller/getServiceInfo';
function Customer(props) {
const [serviceButtons, setServiceButtons] = useState([]);
// here I get the axios async call \/\/
const serviceInfo = getServiceDetails();
// it returns the promise unfullfilled on render.
if (typeof serviceInfo !== 'undefined' && serviceInfo.length > 0) {
let buttonArray = [];
for (let i = 0; i < serviceInfo.length; i++) {
buttonArray.push(
<button
onClick={() => {
console.log('test');
}}
key={i}
>
{serviceInfo[i].name}
</button>
);
}
setServiceButtons(buttonArray);
}
return (
<div id="customer_card">
<h2>Customer overview:</h2>
<p>
Name: {props.db?.firstname + ' ' + props.db?.lastname} <br />
Phone: {props.db?.phone} <br />
Email: {props.db?.email}
</p>
<div className="newbookingbuttons">
<h3>New reservations:</h3>
{serviceButtons}
</div>
<div className="list_of_bookings">
<h3>Current bookings:</h3>
<p>
Here we list all bookings that are current with possible changes
<button
onClick={() => {
// this button does not work... (tried to check if it was different with a click)
let buttonArray = [];
for (let i = 0; i < serviceInfo.length; i++) {
buttonArray.push(
<button
onClick={() => {
console.log('test');
}}
key={i}
>
{serviceInfo[i].name}
</button>
);
}
setServiceButtons(buttonArray);
}}
>
Manual
</button>
</p>
</div>
</div>
);
}
export default Customer;
console:
PromiseĀ {<pending>}Customer.js:29 undefined
Customer.js:30 []
Customer.js:28 PromiseĀ {<pending>}__proto__: Promise[[PromiseState]]: "fulfilled"[[PromiseResult]]: Array(1)
Customer.js:29 undefined

Calling onClick (to setAttribute) inside Array.map() Throws an error TypeError: Cannot read property 'toLowerCase' of undefined

I am building a Gutenberg Block in Wordpress that uses react and what I am trying to achive is that when a div is clicked it should setAttributes with subpage id. the array.map() is used to create each dive with dynamic value that has to be clicked.
const { registerBlockType } = wp.blocks; // Import registerBlockType() from wp.blocks
const { RichText } = wp.editor;
const { withSelect } = wp.data;
const { Fragment, Component } = wp.element;
const {InspectorControls, PanelColorSettings, BlockControls, BlockAlignmentToolbar,MediaUpload } = wp.editor;
const {
RangeControl,
SelectControl,
TextControl,
ToggleControl,
Dashicon,
IconButton,
Button,
Toolbar,
PanelBody,
RadioControl,
TabPanel,
} = wp.components
const {
attributes,
setAttributes,
} = this.props;
const {
subpagesArray,
indexArray,
align,
currentSelected,
displayPageExcerpt,
displayPageIcon,
displayPageTitle
} = attributes;
<section className="gosign-content-nav-block">
{subpagesArray.map(subpage => {
return (
<div className="subpage-block" onClick ={() => {setAttributes({currentSelected : subpage.id});}} >
{displayPageIcon &&
<div className="subpage-icon">
<span className="accessibility">
<i className="material-icons">accessibility</i>
</span>
</div>
}
{displayPageTitle &&
<div className="subpage-title">
<p>{subpage.title.rendered}</p>
</div>
}
</div>
)
})}
</section>
In my opinion this is some 'this' reffrence issue. It would be great if I can get a quick help.

Categories

Resources