useEffect triggers function several times with proper dependencies - reactjs

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.

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.

React Typewriter effect doesn't reset

I have created a typewriting effect with React and it works perfectly fine. However, when I change the language with i18n both texts don't have the same length and it keeps writing until both texts have the same length and then it changes the language and starts the effect again.
How can I reset the input when the language has changed? How can I reset the input when the component has been destroyed?
I have recorded a video
I have the same issue when I change from one page to another, as both pages have different texts and they don't have the same length.
Here code of my component
export const ConsoleText = ({text, complete = false}) => {
const [currentText, setCurrentText] = useState("");
const translatedText = i18n.t(text);
const index = useRef(0);
useEffect(() => {
if (!complete && currentText.length !== translatedText.length) {
const timeOut = setTimeout(() => {
setCurrentText((value) => value + translatedText.charAt(index.current));
index.current++;
}, 20);
return () => {
clearTimeout(timeOut);
}
} else {
setCurrentText(translatedText);
}
}, [translatedText, currentText, complete]);
return (
<p className="console-text">
{currentText}
</p>
);
};
You are telling react to do setCurrentText(translatedText) only when it is complete or when the compared text lengths are equal, so yes it continues to write until this moment.
To reset your text when text changes, try creating another useEffect that will reset your states :
useEffect(() => {
index.current = 0;
setCurrentText('');
}, [text]);
Now, I actually did this exact same feature few days ago, here is my component if it can help you :
import React from 'react';
import DOMPurify from 'dompurify';
import './text-writer.scss';
interface ITextWriterState {
writtenText: string,
index: number;
}
const TextWriter = ({ text, speed }: { text: string, speed: number }) => {
const initialState = { writtenText: '', index: 0 };
const sanitizer = DOMPurify.sanitize;
const [state, setState] = React.useState<ITextWriterState>(initialState);
React.useEffect(() => {
if (state.index < text.length - 1) {
const animKey = setInterval(() => {
setState(state => {
if (state.index > text.length - 1) {
clearInterval(animKey);
return { ...state };
}
return {
writtenText: state.writtenText + text[state.index],
index: state.index + 1
};
});
}, speed);
return () => clearInterval(animKey);
}
}, []);
// Reset the state when the text is changed (Language change)
React.useEffect(() => {
if (text.length > 0) {
setState(initialState);
}
}, [text])
return <div className="text-writer-component"><span className="text" dangerouslySetInnerHTML={{ __html: sanitizer(state.writtenText) }} /></div>
}
export default TextWriter;
The translation is made outside of the component so you can pass any kind of text to the component.

Removing d3-flame-graph when loading new data

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
)
...
}

How to bind context to React Functional Component

I have a function that I call from a child component callback. I'm trying to access some state variable but variables are undefined. I think the issue is when the child component callback the function context it not bind to the parent component. How to do this.
It is sure that myVariable is set before myFunciton is called.
const MyParentView = props => {
const[myVariable, setMyVariable] = useState(undefined)
const onTextFieldChange = val => {
setMyVariable(val)
}
const myFunction = () => {
// myVariable is set to some value by this time
console.log(myVariable)
// But it logs undefined
}
return (
<Input onChange={e => onTextFieldChange(e.target.value)}
<MyChildComponent getData={()=>myFunction()}/>
)
}
Following is the child component ( The actual one )
// #flow
import React, { useEffect, useRef } from "react"
import { get } from "lodash"
type Props = {
children: any,
getData?: Function,
threshold?: number
}
const InfiniteScroll = ({ children, getData, threshold = 0.9 }: Props) => {
const listRef = useRef()
useEffect(() => {
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
}, [])
useEffect(() => {
if (listRef.current) {
const bottom = listRef.current.getBoundingClientRect().bottom
const height =
window.innerHeight || get(document, "documentElement.clientHeight")
if (bottom <= height) {
getData && getData()
}
}
})
const handleScroll = () => {
const winScroll =
get(document, "body.scrollTop") ||
get(document, "documentElement.scrollTop")
const height =
get(document, "documentElement.scrollHeight") -
get(document, "documentElement.clientHeight")
const scrolled = winScroll / height
if (scrolled >= threshold) {
getData && getData()
}
}
return <div ref={listRef}>{children}</div>
}
export default InfiniteScroll
Try returning a closure in your myFunction like this:
const myFunction = () => {
return function() {
// myVariable is set to some value by this time
console.log(myVariable)
// But it logs undefined
}
}

When to use hooks? is worth it that example?

I have write a hook to check if browser is IE, so that I can reutilize the logic instead of write it in each component..
const useIsIE = () => {
const [isIE, setIsIE] = useState(false);
useEffect(() => {
const ua = navigator.userAgent;
const isIe = ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
setIsIE(isIe);
}, []);
return isIE;
}
export default useIsIE;
Is it worth it to use that hook?
Im not sure if is good idea because that way, Im storing a state and a effect for each hook call (bad performane?) when I can simply use a function like that:
export default () => ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
What do you think? is worth it use that hook or not?
If not, when should I use hooks and when not?
ty
No. Not worth using the hook.
You'd need to use a hook when you need to tab into React's underlying state or lifecycle mechanisms.
Your browser will probably NEVER change during a session so just creating a simple utility function/module would suffice.
I would recommend to set your browser checks in constants and not functions, your browser will never change.
...
export const isChrome = /Chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor);
export const isIOSChrome = /CriOS/.test(userAgent);
export const isMac = (navigator.platform.toUpperCase().indexOf('MAC') >= 0);
export const isIOS = /iphone|ipad|ipod/.test(userAgent.toLowerCase());
...
This is a simple hook that checks if a element has been scrolled a certain amount of pixels
const useTop = (scrollable) => {
const [show, set] = useState(false);
useEffect(() => {
const scroll = () => {
const { scrollTop } = scrollable;
set(scrollTop >= 50);
};
const throttledScroll = throttle(scroll, 200);
scrollable.addEventListener('scroll', throttledScroll, false);
return () => {
scrollable.removeEventListener('scroll', throttledScroll, false);
};
}, [show]);
return show;
};
Then you can use it in a 'To Top' button to make it visible
...
import { tween } from 'shifty';
import useTop from '../../hooks/useTop';
// scrollRef is your scrollable container ref (getElementById)
const Top = ({ scrollRef }) => {
const t = scrollRef ? useTop(scrollRef) : false;
return (
<div
className={`to-top ${t ? 'show' : ''}`}
onClick={() => {
const { scrollTop } = scrollRef;
tween({
from: { x: scrollTop },
to: { x: 0 },
duration: 800,
easing: 'easeInOutQuart',
step: (state) => {
scrollRef.scrollTop = state.x;
},
});
}}
role="button"
>
<span><ChevronUp size={18} /></span>
</div>
);
};

Resources