Removing d3-flame-graph when loading new data - reactjs

Recently I picked up a project that has d3-flame-graph on it and the graph is displayed according to the Filters defined on another component.
My issue is that when searching with new parameters I can't seem to clean the previous chart and I was wondering if someone could help me. Basically what I'm having right now is, when I first enter the page, the loading component, then I have my graph and when I search for a new date I have the loading component but on top of that I still have the previous graph
I figured I could use flamegraph().destroy() on const updateGraph but nothing is happening
import React, { FC, useEffect, useRef, useState, useCallback } from 'react'
import { useParams } from 'react-router-dom'
import moment from 'moment'
import * as d3 from 'd3'
import { flamegraph } from 'd3-flame-graph'
import Filters, { Filter } from '../../../../../../components/Filters'
import { getFlamegraph } from '../../../../../../services/flamegraph'
import { useQueryFilter } from '../../../../../../hooks/filters'
import FlamegraphPlaceholder from '../../../../../../components/Placeholders/Flamegraph'
import css from './flamegraph.module.css'
import ToastContainer, {
useToastContainerMessage,
} from '../../../../../../components/ToastContainer'
const defaultFilters = {
startDate: moment().subtract(1, 'month'),
endDate: moment(),
text: '',
limit: 10,
}
const getOffSet = (divElement: HTMLDivElement | null) => {
if (divElement !== null) {
const padding = 100
const minGraphHeight = 450
// ensure that the graph has a min height
return Math.max(
window.innerHeight - divElement.offsetTop - padding,
minGraphHeight
)
} else {
const fallBackNavigationHeight = 300
return window.innerHeight - fallBackNavigationHeight
}
}
const Flamegraph: FC = () => {
const [queryFilters, setQueryFilters] = useQueryFilter(defaultFilters)
const [fetching, setFetching] = useState(false)
const [graphData, setGraphData] = useState()
const {
messages: toastMessages,
addMessage: addMessageToContainer,
removeMessage: removeMessageFromContainer,
} = useToastContainerMessage()
const flameContainerRef = useRef<HTMLDivElement | null>(null)
const flameRef = useRef<HTMLDivElement | null>(null)
const graphRef = useRef<any>()
const graphDataRef = useRef<any>()
const timerRef = useRef<any>()
const { projectId, functionId } = useParams()
let [sourceId, sourceLine] = ['', '']
if (functionId) {
;[sourceId, sourceLine] = functionId.split(':')
}
const createGraph = () => {
if (flameContainerRef.current && flameRef.current) {
graphRef.current = flamegraph()
.width(flameContainerRef.current.offsetWidth)
.height(getOffSet(flameRef.current))
.cellHeight(30)
.tooltip(false)
.setColorMapper(function(d, originalColor) {
// Scale green component proportionally to box width (=> the wider the redder)
let greenHex = (192 - Math.round((d.x1 - d.x0) * 128)).toString(16)
return '#FF' + ('0' + greenHex).slice(-2) + '00'
})
}
}
const updateGraph = (newData: any) => {
setGraphData(newData)
graphDataRef.current = newData
if (graphRef.current) {
if (newData === null) {
graphRef.current.destroy()
graphRef.current = null
} else {
d3.select(flameRef.current)
.datum(newData)
.call(graphRef.current)
}
}
}
const fetchGraph = (filters: Filter) => {
setFetching(true)
getFlamegraph(
Number(projectId),
filters.startDate ? filters.startDate.unix() : 0,
filters.endDate ? filters.endDate.unix() : 0,
sourceId,
sourceLine
)
.then(graphData => {
if (!graphRef.current) {
createGraph()
}
updateGraph(graphData)
})
.catch(({ response }) => {
updateGraph(null)
if (response.data) {
addMessageToContainer(response.data.message, true)
}
})
.finally(() => {
setFetching(false)
})
}
const onResize = useCallback(() => {
clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => {
if (graphRef.current && flameContainerRef.current) {
graphRef.current.width(flameContainerRef.current.offsetWidth)
d3.select(flameRef.current)
.datum(graphDataRef.current)
.call(graphRef.current)
}
}, 500)
}, [])
useEffect(() => {
fetchGraph(queryFilters)
window.addEventListener('resize', onResize)
return () => {
window.removeEventListener('resize', onResize)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const onChangeFilters = (filters: Filter) => {
setQueryFilters(filters)
fetchGraph(filters)
}
return (
<div className={css.host}>
<Filters
defaultValues={queryFilters}
searching={fetching}
onSearch={onChangeFilters}
/>
<div className={css.flameBox}>
<div className={css.flameContainer} ref={flameContainerRef}>
<div ref={flameRef} />
</div>
{fetching || !graphData ? (
<FlamegraphPlaceholder loading={fetching} />
) : null}
</div>
<ToastContainer
messages={toastMessages}
toastDismissed={removeMessageFromContainer}
/>
</div>
)
}
export default Flamegraph

Firstly, flamegraph() creates a new instance of flamegraph, you'd need to use graphref.current.destroy(). Secondly, you'd want to destroy this not when the data has already been loaded, but just as it starts to load, right? Because that's the operation that takes time.
Consider the following:
const cleanGraph = () => {
if (graphref.current !== undefined) {
graphref.current.destroy()
}
}
const fetchGraph = (filters: Filter) => {
setFetching(true)
cleanGraph()
getFlamegraph(
Number(projectId),
filters.startDate ? filters.startDate.unix() : 0,
filters.endDate ? filters.endDate.unix() : 0,
sourceId,
sourceLine
)
...
}

Related

SetState using a filter is removing everything from the array

import { useEffect } from 'react';
import { getXPosition, getYPosition } from 'helpers/location-helper';
import { useState } from 'react';
import { AircraftWrapper } from './aircraft.styles';
const Aircraft = ({ flightData, setFlight, flights }) => {
const flightId = flightData.flightId;
const [location, setLocation] = useState(flightData.coordinate);
const [currentState, setState] = useState(true);
useEffect(() => {
if(currentState === false) {
setFlight(flights.filter(item => item.flightId !== flightData?.flightId));
}
const interval = setInterval(() => {
setLocation({...location, x: (location.x+(10*Math.cos(Math.PI/180*flightData?.heading))), y: location.y+(10*Math.sin(Math.PI/180*flightData?.heading))});
if (location.x < -5 || location.y < 0 || location.x > 105 || location.y > 150) {
setState(false);
}
}, 1000);
return() => clearInterval(interval);
}, [location, flightData.heading]);
return(
<AircraftWrapper
data-testid="aircraft"
style={{left: `${getXPosition(location?.x)}`, top: `${getYPosition(location?.y)}`}}
heading={flightData?.heading+85}
/>
)
}
export default Aircraft;
//(150) + Math.cos(flightData.heading)*location.y
The error is in the if(currentState === false). It causes all aircrafts to be removed from the screen. What I am trying to do is based off of a timer, update the position of the aircraft and use a transition to move it forward until it goes past the map boundaries. as soon as it hits the map boundaries, it will disappear from the screen.

merging custom hooks for handling scroll

I am trying to merge these two hooks to get the direction and the percentage of the scroll.
useScrollPercentage.js
import { useRef, useState, useEffect } from "react";
export default function useScrollPercentage() {
const scrollRef = useRef(null);
const [scrollPercentage, setScrollPercentage] = useState(NaN);
const reportScroll = e => {
setScrollPercentage(getScrollPercentage(e.target));
};
useEffect(
() => {
const node = scrollRef.current;
if (node !== null) {
node.addEventListener("scroll", reportScroll, { passive: true });
if (Number.isNaN(scrollPercentage)) {
setScrollPercentage(getScrollPercentage(node));
}
}
return () => {
if (node !== null) {
node.removeEventListener("scroll", reportScroll);
}
};
},
[scrollPercentage]
);
return [scrollRef, Number.isNaN(scrollPercentage) ? 0 : scrollPercentage];
}
function getScrollPercentage(element) {
if (element === null) {
return NaN;
}
const height = element.scrollHeight - element.clientHeight;
return Math.round((element.scrollTop / height) * 100);
}
useScrollDirection.js
import {useState, useEffect} from 'react';
import _ from 'lodash';
export default function useScrollDirection({
ref,
threshold,
debounce,
scrollHeightThreshold,
}) {
threshold = threshold || 10;
debounce = debounce || 10;
scrollHeightThreshold = scrollHeightThreshold || 0;
const [scrollDir, setScrollDir] = useState(null);
const debouncedSetScrollDir = _.debounce(setScrollDir, debounce);
useEffect(() => {
let lastScrollY = ref?.current?.scrollTop;
let lastScrollDir;
let ticking = false;
const hasScrollHeightThreshold =
ref?.current?.scrollHeight - ref?.current?.clientHeight >
scrollHeightThreshold;
const updateScrollDir = () => {
const scrollY = ref?.current?.scrollTop;
if (
Math.abs(scrollY - lastScrollY) < threshold ||
!hasScrollHeightThreshold
) {
ticking = false;
return;
}
const newScroll = scrollY > lastScrollY ? 'Down' : 'Up';
if (newScroll !== lastScrollDir) {
debouncedSetScrollDir(newScroll);
}
lastScrollY = scrollY > 0 ? scrollY : 0;
lastScrollDir = newScroll;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
ref?.current?.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return scrollDir;
}
Currently I am using it like this
App.js
const [scrollRef, scrollPercentage] = useScrollPercentage();
const scrollDirection = useScrollDirection({ref: scrollRef});
const handleScroll = () => {
console.log(scrollPercentage, scrollDirection);
}
return (
<Modal ref={scrollRef} onScroll={handleScroll}>
// Scrollable Content
</Modal>
)
The problem is that since I don't want to trigger useScrollDirection for every scroll percentage unless the scroll direction changed only it should trigger. Also is it possible to merge these hooks?
Any help is appreciated

React has detected a change in the order of Hooks error

I tried implementing infinite scroll using useSWRInfinite hook.
Found a code from an Youtube tutorial 👈 and made little alteration.
But got an error named "React has detected a change in the order of Hooks called by InfiniteDataList."
2 hours of debugging -- no solution found.
Picture of the error
Youtube tutorial code --> https://github.com/gdangelo/micro-blogging-workshop/blob/main/components/InfiniteDataList.js
Youtube tutorial link --> https://www.youtube.com/watch?v=FsngdxyvFrQ
MY CODE:
InfiniteDataList.js
import React, { useEffect, useRef } from "react";
import { useInfiniteQuery } from "../../hooks";
import MessageWrapper from "../../UI/MessageWrapper";
import { isInViewport } from "../../Utility/windowUtils";
import { useDebouncedCallback } from "use-debounce";
import DefaultListItemComponent from "./components/DefaultListItemComponent";
import DefaultContainerComponent from "./components/DefaultContainerComponent";
import DefaultLoadMoreComponent from "./components/DefaultLoadMoreComponent";
const InfiniteDataList = ({
queryKey,
initialData = [],
listItemComponent: ListItemComponent = DefaultListItemComponent,
containerComponent: ContainerComponent = DefaultContainerComponent,
onError = () => {},
onEmpty = () => {},
onEmptyComponent: OnEmptyComponent = null,
onNoMoreData = () => {},
noMoreDataComponent: NoMoreDataComponent = null,
isAutoLoadMoreAtEnd = true,
autoLoadMoreAtEndOptions: {
timeout = 500,
onLoadMoreDetected = () => {},
} = {},
loadMoreComponent: LoadMoreComponent = DefaultLoadMoreComponent,
}) => {
// hooks
const {
data,
error,
hasNextPage,
fetchNextPage,
isFetchingInitialData,
isFetchingNextPageData,
} = useInfiniteQuery(queryKey, { initialData });
const moreRef = useRef();
const loadMore = useDebouncedCallback(() => {
if (isInViewport(moreRef.current)) {
onLoadMoreDetected();
fetchNextPage();
}
}, timeout);
const getLoadMoreRef = () => moreRef;
useEffect(() => {
if (isAutoLoadMoreAtEnd) {
window.addEventListener("scroll", loadMore);
}
return () => window.removeEventListener("scroll", loadMore);
}, []);
// some configuration
OnEmptyComponent = OnEmptyComponent && (() => <h4>No Details found</h4>);
NoMoreDataComponent =
NoMoreDataComponent &&
(() => <MessageWrapper message="No More Data found !" />);
// helper utils
const infiniteQueryProps = {
data,
error,
hasNextPage,
fetchNextPage,
isFetchingInitialData,
isFetchingNextPageData,
};
// if error occurs
if (error) {
onError(error);
console.log("error");
}
// no data found
if (!isFetchingInitialData && data?.length === 0) {
onEmpty();
console.log(typeof OnEmptyComponent);
return <OnEmptyComponent />;
}
// no more data to load
if (!hasNextPage) {
onNoMoreData();
}
return (
<ContainerComponent loading={isFetchingInitialData}>
{data?.map((item, index) => (
<ListItemComponent key={index} {...item} />
))}
{hasNextPage ? (
<LoadMoreComponent
{...infiniteQueryProps}
getLoadMoreRef={getLoadMoreRef}
/>
) : (
<NoMoreDataComponent {...infiniteQueryProps} />
)}
</ContainerComponent>
);
};
export default InfiniteDataList;
useInfiniteQuery.js
import useSWRInfinite from "swr/infinite";
import { axiosInstance } from "../Utility/axiosInstance";
function getFetcher(requestType = "get") {
return (url, dataToPost) =>
axiosInstance[requestType](url, dataToPost).then((res) => res.data);
}
export function useInfiniteQuery(
queryKey,
{ initialData, requestType = "get" }
) {
const { data, error, size, setSize } = useSWRInfinite(
(pageIndex, previousPageData) => {
// reached the end
if (previousPageData && !previousPageData.after) return null;
// first page
if (pageIndex === 0) return queryKey;
// next pages
const search = queryKey.includes("?");
return `${queryKey}${search ? "$" : "?"}cursor=${encodeURIComponent(
JSON.stringify(previousPageData.after)
)}`;
},
getFetcher(requestType),
initialData
);
// to fetch next page from react component
function fetchNextPage() {
setSize((prev) => prev + 1);
}
// flatten all the data obtained so far to a single array
const flattenPages = data?.flatMap((page) => page.data) ?? [];
// indicates whether the api will have data for another page
const hasNextPage = !!data?.[size - 1]?.after;
// isLoading for initial request
const isFetchingInitialData = !data && !error;
// isLoading for other requests including the initial request
const isFetchingNextPageData =
isFetchingInitialData ||
(size > 0 && data && typeof data[size - 1] === "undefined");
return {
data: flattenPages,
error,
hasNextPage,
fetchNextPage,
isFetchingInitialData,
isFetchingNextPageData,
};
}
isInViewport.js
// Check if element is visible inside the viewport
export function isInViewport(element) {
if (!element) return false;
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
DefaultLoadMoreComponent.js
import React, { useState } from "react";
const DefaultLoadMoreComponent = ({ getLoadMoreRef = () => {} }) => {
const ref = getLoadMoreRef();
return <div ref={ref} />;
};
export default DefaultLoadMoreComponent;
DefaultListItemComponent.js
import React from "react";
const DefaultListItemComponent = ({ children = [] }) => <div>{children}</div>;
export default DefaultListItemComponent;
DefaultContainerComponent.js
import React from "react";
import AsyncDiv from "../../../UI/AsyncDiv";
const DefaultContainerComponent = ({ children = [], ...rest }) => (
<AsyncDiv {...rest}>{children}</AsyncDiv>
);
export default DefaultContainerComponent;
Component where I render InfiniteDataList component
import React from "react";
import InfiniteDataList from "../../../../../UI/InfiniteDataList";
import PaginatedLeads from "./components/PaginatedLeads";
import { getError } from "../../../../../Utility/apiUtils";
const ViewAllLeads = (props) => {
return (
<InfiniteDataList
initialData={[]}
listItemComponent={PaginatedLeads}
onError={(err) =>
window.flash({ title: getError(err).message, type: "error" })
}
queryKey="/employee/leads"
/>
);
};
export default ViewAllLeads;
PaginatedLeads.js
import React from "react";
const PaginatedLeads = (props) => {
console.log(props);
return <div>PaginatedLeads</div>;
};
export default PaginatedLeads;
It is my mistake.
In useInfiniteQuery.js file, I passed the initial data in wrong format.
Wrong syntax -- see the last before line(initialData)
const { data, error, size, setSize } = useSWRInfinite(
(pageIndex, previousPageData) => {
// reached the end
if (previousPageData && !previousPageData.after) return null;
// first page
if (pageIndex === 0) return queryKey;
// next pages
const search = queryKey.includes("?");
return `${queryKey}${search ? "&" : "?"}cursor=${encodeURIComponent(
JSON.stringify(previousPageData.after)
)}`;
},
getFetcher(requestType),
initialData
);
Right syntax -- see the last before line({ fallbackData: initialData })
const { data, error, size, setSize } = useSWRInfinite(
(pageIndex, previousPageData) => {
// reached the end
if (previousPageData && !previousPageData.after) return null;
// first page
if (pageIndex === 0) return queryKey;
// next pages
const search = queryKey.includes("?");
return `${queryKey}${search ? "&" : "?"}cursor=${encodeURIComponent(
JSON.stringify(previousPageData.after)
)}`;
},
getFetcher(requestType),
{ fallbackData: initialData }
);

Warning: Using UNSAFE_componentWillMount in strict mode is not recommended (upgrade to CRA 4.0.2)

I updated my React application from 16.3+ to React 17 while upgrading to crate-react-app#4.0.2. Everything works as expected, but I see the following in the console:
Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See react-unsafe-component-lifecycles for details.
* Move code with side effects to componentDidMount, and set initial state in the constructor.
Please update the following components: SideEffect(NullComponent)
My App.jsx file:
import React, { useRef, useEffect, useCallback, createRef } from 'react';
import { useDispatch, useSelector, batch } from 'react-redux';
import './App.scss';
import { CountryBox, Error, MasterBox, MetaTags, ModalContainer, ScreenLoader } from '../../components';
import { dataActions, settingsActions, statisticsActions, statisticsUpdatesActions } from '../../store/actions/actions';
import { engineService } from '../../services';
import { coreUtils } from '../../utils';
const App = (props) => {
const dispatch = useDispatch();
// Refs.
const elRefs = useRef([]);
// State variables.
const settingsList = useSelector((state) => state.settings.settingsList);
const loadingList = useSelector((state) => state.settings.loadingList);
const sourcesList = useSelector((state) => state.data.sourcesList);
const countriesList = useSelector((state) => state.data.countriesList);
const { isActive, isRefreshMode, viewType, isDisplayError, activeModalName,
activeModalValue, isReplaceModalMode, isActionLoader } = settingsList;
const { loadingPrecentage, isScreenLoaderComplete } = loadingList;
// Functions to update the state.
const onSetStateCurrentTime = (data) => dispatch(statisticsActions.setStateCurrentTime(data));
const onSetStateSettingsList = (listName, listValues) => dispatch(settingsActions.setStateSettingsList(listName, listValues));
const onSetStateStatisticsField = (fieldName, fieldValue) => dispatch(statisticsActions.setStateStatisticsField(fieldName, fieldValue));
const onSetStateStatisticsList = (statisticsList) => dispatch(statisticsActions.setStateStatisticsList(statisticsList));
const onSetStateStatisticsUpdatesSettingsList = (statisticsUpdatesSettingsList) => dispatch(statisticsUpdatesActions.setStateStatisticsUpdatesSettingsList(statisticsUpdatesSettingsList));
const onSetStateDataCollection = (collectionName, collectionValue) => dispatch(dataActions.setStateDataCollection(collectionName, collectionValue));
const onSetStateInitiateSettings = (data) => {
const { settingsList, loadingList } = data;
batch(() => {
dispatch(settingsActions.setStateSettingsList('settingsList', settingsList));
dispatch(settingsActions.setStateSettingsList('loadingList', loadingList));
});
};
const onSetStateInitiateSources = (data) => {
const { sourcesList, countriesList, countriesNameIdList, statisticsList, settingsList } = data;
batch(() => {
dispatch(dataActions.setStateDataCollection('sourcesList', sourcesList));
dispatch(dataActions.setStateDataCollection('countriesList', countriesList));
dispatch(dataActions.setStateDataCollection('countriesNameIdList', countriesNameIdList));
dispatch(settingsActions.setStateSettingsList('settingsList', settingsList));
dispatch(statisticsActions.setStateStatisticsList(statisticsList));
});
};
const onSetStateUpdateRound = (data) => {
const { countriesList, statisticsList, updateStatisticsUpdatesListResults } = data;
const { statisticsUpdatesList, statisticsUpdatesSettingsList } = updateStatisticsUpdatesListResults;
batch(() => {
dispatch(dataActions.setStateDataCollection('countriesList', countriesList));
dispatch(statisticsActions.setStateStatisticsList(statisticsList));
if (statisticsUpdatesList && statisticsUpdatesList.length > 0) {
dispatch(statisticsUpdatesActions.setStateStatisticsUpdatesList(statisticsUpdatesList));
dispatch(statisticsUpdatesActions.setStateStatisticsUpdatesSettingsList(statisticsUpdatesSettingsList));
}
});
};
const onSetStateActionUpdate = (data) => {
const { countriesList, settingsList } = data;
batch(() => {
dispatch(dataActions.setStateDataCollection('countriesList', countriesList));
dispatch(settingsActions.setStateSettingsList('settingsList', settingsList));
});
};
const onSetStateActionRefresh = (data) => {
const { countriesList, settingsList, statisticsList, updateStatisticsUpdatesListResults } = data;
const { statisticsUpdatesList, statisticsUpdatesSettingsList } = updateStatisticsUpdatesListResults;
batch(() => {
dispatch(dataActions.setStateDataCollection('countriesList', countriesList));
dispatch(settingsActions.setStateSettingsList('settingsList', settingsList));
dispatch(statisticsActions.setStateStatisticsList(statisticsList));
if (statisticsUpdatesList && statisticsUpdatesList.length > 0) {
dispatch(statisticsUpdatesActions.setStateStatisticsUpdatesList(statisticsUpdatesList));
dispatch(statisticsUpdatesActions.setStateStatisticsUpdatesSettingsList(statisticsUpdatesSettingsList));
}
});
};
const onSetStateUpdateCountryVisibility = (data) => {
const { countriesList, countriesNameIdList, statisticsList, statisticsUpdatesList } = data;
batch(() => {
dispatch(dataActions.setStateDataCollection('countriesList', countriesList));
dispatch(dataActions.setStateDataCollection('countriesNameIdList', countriesNameIdList));
dispatch(statisticsActions.setStateStatisticsList(statisticsList));
if (statisticsUpdatesList && statisticsUpdatesList.length > 0) {
dispatch(statisticsUpdatesActions.setStateStatisticsUpdatesList(statisticsUpdatesList));
}
});
};
// Run the engine.
useEffect(() => {
engineService.runEngine({
mode: props.mode,
onSetStateCurrentTime: onSetStateCurrentTime,
onSetStateSettingsList: onSetStateSettingsList,
onSetStateStatisticsField: onSetStateStatisticsField,
onSetStateStatisticsList: onSetStateStatisticsList,
onSetStateStatisticsUpdatesSettingsList: onSetStateStatisticsUpdatesSettingsList,
onSetStateInitiateSettings: onSetStateInitiateSettings,
onSetStateInitiateSources: onSetStateInitiateSources,
onSetStateUpdateRound: onSetStateUpdateRound,
onSetStateDataCollection: onSetStateDataCollection,
onSetStateActionUpdate: onSetStateActionUpdate,
onSetStateActionRefresh: onSetStateActionRefresh,
onSetStateUpdateCountryVisibility: onSetStateUpdateCountryVisibility
});
return () => {
engineService.clearSources();
};
}, []);
// Set loader for each master action.
useEffect(() => {
engineService.updateActionLoader(false);
}, [countriesList]);
// After exit from any modal - Scroll back to the element's vertical position.
const scrollToCountry = useCallback((data) => {
const { action, value } = data;
if (action === 'modal' && !value && activeModalValue && !isReplaceModalMode && activeModalName !== 'country') {
setTimeout(() => {
const offsetTop = elRefs.current.find(c => c.current?.dataset?.countryId === activeModalValue).current.offsetTop;
if (offsetTop > window.innerHeight) {
window.scrollTo(0, offsetTop);
}
}, 10);
}
}, [elRefs, activeModalValue, isReplaceModalMode]);
// Update action on master modal click.
const handleActionClick = useCallback((e) => {
if (!isActionLoader) {
const data = {
action: coreUtils.getAttributeName(e, 'data-action'),
value: coreUtils.getAttributeName(e, 'name'),
id: coreUtils.getAttributeName(e, 'data-country-id')
};
scrollToCountry(data);
engineService.runMasterActionClick(data);
}
}, [elRefs, activeModalValue, isReplaceModalMode]);
// Update action on relevant modal change.
const handleModalActionChange = useCallback((e) => {
engineService.runModalActionUpdate({
modalName: coreUtils.getAttributeName(e, 'data-modal-name'),
action: coreUtils.getAttributeName(e, 'data-action'),
value: coreUtils.getValue(e)
});
}, []);
// Validate all OK to show the data and generate the countries.
const isInitiateComplete = !isDisplayError && countriesList && countriesList.length > 0 && loadingPrecentage === 100;
const renderCountries = useCallback(() => {
const countriesDOM = [];
const refsList = [];
for (let i = 0; i < countriesList.length; i++) {
const country = countriesList[i];
const ref = elRefs.current[i] || createRef();
refsList.push(ref);
countriesDOM.push(
(<CountryBox
key={country.id}
{...country} // React memo works only with separated properties.
isRefreshMode={isRefreshMode}
sourcesList={sourcesList}
onActionClick={handleActionClick}
ref={ref}
/>));
}
elRefs.current = refsList;
return countriesDOM;
}, [countriesList]);
return (
<div className="main">
{MetaTags}
{!isScreenLoaderComplete &&
<ScreenLoader
isActive={isActive}
loadingList={loadingList}
isDisplayError={isDisplayError}
/>
}
{isDisplayError &&
<Error
isDisplayError={isDisplayError}
/>
}
{activeModalName &&
<ModalContainer
onActionClick={handleActionClick}
onActionChange={handleModalActionChange}
/>
}
{isInitiateComplete &&
<div className="page">
<div className="main-container">
<div className={`container ${viewType} f32 f32-extra locations`}>
<MasterBox
onActionClick={handleActionClick}
/>
{renderCountries()}
</div>
</div>
</div>
}
</div>
);
};
export default App;
How can I fix this problem?
OK, I solved it.
The issue was with one of the components named MetaTags:
MetaTags.jsx
import React from 'react';
import { Helmet } from 'react-helmet';
import { timeUtils } from '../../../utils';
const MetaTags =
(<Helmet>
<title data-rh="true">World Covid 19 Data | Covid 19 World Data | {timeUtils.getTitleDate()}</title>
</Helmet>);
export default MetaTags;
The react-helmet package is outdated, and I needed to install 'react-helmet-async' instead, and change the code to:
initiate.jsx
app = (
<HelmetProvider>
<Suspense fallback={null}>
<Provider store={createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)))}>
<Helmet>
<title data-rh="true">Dynamic title {timeUtils.getTitleDate()}</title>
</Helmet>
<BrowserRouter>
{component}
</BrowserRouter>
</Provider>
</Suspense>
</HelmetProvider>
);
This solved my issue and the warning was gone.

useEffect triggers function several times with proper dependencies

i've got Tabs component, it has children Tab components. Upon mount it calculates meta data of Tabs and selected Tab. And then sets styles for tab indicator. For some reason function updateIndicatorState triggers several times in useEffect hook every time active tab changes, and it should trigger only once. Can somebody explain me what I'm doing wrong here? If I remove from deps of 2nd useEffect hook function itself and add a value prop as dep. It triggers correctly only once. But as far as I've read docs of react - I should not cheat useEffect dependency array and there are much better solutions to avoid that.
import React, { useRef, useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { defProperty } from 'helpers';
const Tabs = ({ children, value, orientation, onChange }) => {
console.log(value);
const indicatorRef = useRef(null);
const tabsRef = useRef(null);
const childrenWrapperRef = useRef(null);
const valueToIndex = new Map();
const vertical = orientation === 'vertical';
const start = vertical ? 'top' : 'left';
const size = vertical ? 'height' : 'width';
const [mounted, setMounted] = useState(false);
const [indicatorStyle, setIndicatorStyle] = useState({});
const [transition, setTransition] = useState('none');
const getTabsMeta = useCallback(() => {
console.log('getTabsMeta');
const tabsNode = tabsRef.current;
let tabsMeta;
if (tabsNode) {
const rect = tabsNode.getBoundingClientRect();
tabsMeta = {
clientWidth: tabsNode.clientWidth,
scrollLeft: tabsNode.scrollLeft,
scrollTop: tabsNode.scrollTop,
scrollWidth: tabsNode.scrollWidth,
top: rect.top,
bottom: rect.bottom,
left: rect.left,
right: rect.right,
};
}
let tabMeta;
if (tabsNode && value !== false) {
const wrapperChildren = childrenWrapperRef.current.children;
if (wrapperChildren.length > 0) {
const tab = wrapperChildren[valueToIndex.get(value)];
tabMeta = tab ? tab.getBoundingClientRect() : null;
}
}
return {
tabsMeta,
tabMeta,
};
}, [value, valueToIndex]);
const updateIndicatorState = useCallback(() => {
console.log('updateIndicatorState');
let _newIndicatorStyle;
const { tabsMeta, tabMeta } = getTabsMeta();
let startValue;
if (tabMeta && tabsMeta) {
if (vertical) {
startValue = tabMeta.top - tabsMeta.top + tabsMeta.scrollTop;
} else {
startValue = tabMeta.left - tabsMeta.left;
}
}
const newIndicatorStyle =
((_newIndicatorStyle = {}),
defProperty(_newIndicatorStyle, start, startValue),
defProperty(_newIndicatorStyle, size, tabMeta ? tabMeta[size] : 0),
_newIndicatorStyle);
if (isNaN(indicatorStyle[start]) || isNaN(indicatorStyle[size])) {
setIndicatorStyle(newIndicatorStyle);
} else {
const dStart = Math.abs(indicatorStyle[start] - newIndicatorStyle[start]);
const dSize = Math.abs(indicatorStyle[size] - newIndicatorStyle[size]);
if (dStart >= 1 || dSize >= 1) {
setIndicatorStyle(newIndicatorStyle);
if (transition === 'none') {
setTransition(`${[start]} 0.3s ease-in-out`);
}
}
}
}, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);
useEffect(() => {
const timeout = setTimeout(() => {
setMounted(true);
}, 350);
return () => {
clearTimeout(timeout);
};
}, []);
useEffect(() => {
if (mounted) {
console.log('1st call mounted');
updateIndicatorState();
}
}, [mounted, updateIndicatorState]);
let childIndex = 0;
const childrenItems = React.Children.map(children, child => {
const childValue = child.props.value === undefined ? childIndex : child.props.value;
valueToIndex.set(childValue, childIndex);
const selected = childValue === value;
childIndex += 1;
return React.cloneElement(child, {
selected,
indicator: selected && !mounted,
value: childValue,
onChange,
});
});
const styles = {
[size]: `${indicatorStyle[size]}px`,
[start]: `${indicatorStyle[start]}px`,
transition,
};
console.log(styles);
return (
<>
{value !== 2 ? (
<div className={`tabs tabs--${orientation}`} ref={tabsRef}>
<span className="tab__indicator-wrapper">
<span className="tab__indicator" ref={indicatorRef} style={styles} />
</span>
<div className="tabs__wrapper" ref={childrenWrapperRef}>
{childrenItems}
</div>
</div>
) : null}
</>
);
};
Tabs.defaultProps = {
orientation: 'horizontal',
};
Tabs.propTypes = {
children: PropTypes.node.isRequired,
value: PropTypes.number.isRequired,
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
onChange: PropTypes.func.isRequired,
};
export default Tabs;
useEffect(() => {
if (mounted) {
console.log('1st call mounted');
updateIndicatorState();
}
}, [mounted, updateIndicatorState]);
This effect will trigger whenever the value of mounted or updateIndicatorState changes.
const updateIndicatorState = useCallback(() => {
...
}, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);
The value of updateIndicatorState will change if any of the values in its dep array change, namely getTabsMeta.
const getTabsMeta = useCallback(() => {
...
}, [value, valueToIndex]);
The value of getTabsMeta will change whenever value or valueToIndex changes. From what I'm gathering from your code, value is the value of the selected tab, and valueToIndex is a Map that is re-defined on every single render of this component. So I would expect the value of getTabsMeta to be redefined on every render as well, which will result in the useEffect containing updateIndicatorState to run on every render.

Resources