I have the isDesktop boolean that should be set to true or false depending on the sceensize and this works however on initial render it doesn't set to true/false, how can I set this on intial render?
const [isVisible, setIsVisible] = useState(false)
const [isDesktop, setIsDesktop] = useState(window.innerWidth)
console.log(isDesktop)
useEffect(() => {
window.addEventListener("scroll", UpdateScrollPosition);
window.addEventListener("resize", displayWindowSize);
return () => window.removeEventListener("scroll", UpdateScrollPosition);
}, []);
const UpdateScrollPosition = () => {
const scrollPos = window.scrollY
if( scrollPos < 520) {
return setIsVisible(false)
}else if (scrollPos >= 520 && scrollPos <= 1350) {
return setIsVisible(true)
}else if (scrollPos > 1350) {
return setIsVisible(false)
}
}
const displayWindowSize = () => {
let w = window.innerWidth;
if(w >= 500) {
return setIsDesktop(true)
} else {
return setIsDesktop(false)
}
}
window.innerWidth is not a boolean. If you intented to set it to true for a value other than 0 you can do:
const [isDesktop, setIsDesktop] = useState(!!window.innerWidth)
You can also compare it to your breakpoint:
const [isDesktop, setIsDesktop] = useState(window.innerWidth >= 500);
EDIT:
If you want to use isDesktop in the UpdateScrollPosition handler you need to unregister the old handler and register a new handler as a listener, when isDesktop has changed:
const UpdateScrollPosition = useCallback(() => {
// this now depends on isDesktop
console.log(isDesktop);
// .... other code
}, [isDesktop]); // IMPORTANT: add isDesktop here as a dependency
useEffect(() => {
window.addEventListener("scroll", UpdateScrollPosition);
return () => window.removeEventListener("scroll", UpdateScrollPosition);
}, [UpdateScrollPosition]); // IMPORTANT: Add UpdateScrollPosition here as a dependency
What does this?:
useCallback will recreate your handler when the isDesktop dependency changes. Your effect will re-bind the handlers when UpdateScrollPosition changes (which is always the case when isDesktop changes as we added it as a dependency there).
Related
Here i define seconds and timeArr
const [spaceEventCounter, setSpaceEventCounter] = useState(0);
const [isRunning, setIsRunning] = useState(false); // isRunning = true -> secondsCounter = running
const [timeArr, setTimeArr] = useState([8.55, 9.55, 10.55, 11.55, 12.55]);
const [seconds, setSeconds] = useState(0);
this useEffect starts and stops a counter if isRunning = true/false
useEffect(() => {
let interval;
if (isRunning) {
interval = setInterval(() => {
setSeconds(seconds => Number((seconds + 0.01).toFixed(2)));
}, 10);
} else {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [isRunning]);
the problem occurs when setTimeArr is run within the handleKeyUp function in the third if statment
useEffect(() => {
const handleKeyDown = (event) => {
if (event.code !== "Space") return;
if (spaceEventCounter === 0) {
setSpaceEventCounter(1);
}
if (spaceEventCounter === 2) {
setIsRunning(false);
setTimeArr([...timeArr, seconds]);
setSpaceEventCounter(3);
}
};
const handleKeyUp = (event) => {
if (event.code !== "Space") return;
if (spaceEventCounter === 1) {
setIsRunning(true)
setSpaceEventCounter(2);
} else if (spaceEventCounter === 3) {
setSpaceEventCounter(0);
}
} ;
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("keyup", handleKeyUp);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("keyup", handleKeyUp);
};
}, [spaceEventCounter]);
I dont know what to try :(
What I see is that you declare your functions inside useEffect.
This useEffect hook has spaceEventCounter as dependency.
In other words, every time spaceEventCounter changes, you redeclare your functions and add event listeners, while you really need to add the event listeners once the component is mounted.
I would try using the last useEffect with an empty dependency array.
I'm trying to achieve smooth animation of searchbar (MUI Autocomplete). This should work only on Smartphones (Screen < 600px).
Here is an example (it is very buggy and open it on smartphone to see the animation): https://react-zxuspr-gjq5w8.stackblitz.io/
And here is my implementation, but I've a few problems with that:
The interval does not reset on dropdown close.
The React.useEffect() dependency is set to searchActive, which is changed dynamically.
I tried calling the callback function of React.useState(), but since the component is not destroyed, I am not sure if it makes sense.
The width of dropdown, which is also changed in the setInterval() function, is not smooth at all.
https://stackblitz.com/edit/react-zxuspr-gjq5w8?file=demo.js
Here is part of the component where the logic is implemented:
export function PrimarySearchAppBar() {
const [searchActive, setSearchActive] = React.useState(null);
const [acPaperWidth, setAcPaperWidth] = React.useState(null);
const [acPaperTransX, setAcPaperTransX] = React.useState(0);
const AcRef = React.useRef(null);
const isMobile = useMediaQuery(useTheme().breakpoints.down('sm'));
const options = top100Films.map((option) => {
const group = option.group.toUpperCase();
return {
firstLetter: group,
...option,
};
});
React.useLayoutEffect(() => {
if (AcRef.current) {
setAcPaperWidth(AcRef.current.offsetWidth);
}
console.log(acPaperWidth);
}, [AcRef]);
let interval;
React.useEffect(() => {
if (searchActive) {
if (acPaperTransX <= 39) {
interval = setInterval(() => {
setAcPaperWidth(AcRef.current.offsetWidth);
setAcPaperWidth((acPaperTransX) => acPaperTransX + 1);
if (acPaperTransX >= 38) {
clearInterval(interval);
}
console.log(acPaperTransX);
}, 10);
}
} else {
setAcPaperTransX(0);
clearInterval(interval);
}
}, [searchActive]);
return (
<>Hello World</>
);
}
The problem was that the interval variable was defined outside the React.useEffect() hook, so its value was not preserved on re-renders.
I was able to fix that by using React.useRef():
const intervalRef = React.useRef();
const acPaperTransXRef = React.useRef(acPaperTransX);
React.useEffect(() => {
if (searchActive) {
if (acPaperTransXRef.current <= 39) {
intervalRef.current = setInterval(() => {
setAcPaperWidth(AcRef.current.offsetWidth);
acPaperTransXRef.current += 0.1;
if (acPaperTransXRef.current >= 38) {
clearInterval(intervalRef.current);
acPaperTransXRef.current = 0;
}
console.log(acPaperTransXRef.current);
}, 2);
}
} else {
setAcPaperTransX(0);
acPaperTransXRef.current = 0;
clearInterval(intervalRef.current);
}
return () => clearInterval(intervalRef.current);
}, [searchActive]);
So I've followed this:
https://www.pinkdroids.com/blog/moving-header-react-hooks-context/
And managed to get it working on one app.
I'm not replicating what I did in another app and it's not working.
I've got as far as working out that my previousScrollTop and currentScrollTop are always the same – therefore no direction is being added
But why are they the same!!!
I've gone as far as copy and pasting the code so no issues there.
Could this be a problem with memo?
with my lodash import?
Not getting any errors.
UPDATE*
The problem is either with useMemo, or useRef in my useScroll hook.
const getScrollPosition = () => {
if (typeof window === 'undefined') {
return 0
}
return (
window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop ||
0
)
}
export const useScroll = (timeout = 250) => {
const defaultScrollTop = useMemo(() => getScrollPosition(), [])
const previousScrollTop = useRef(defaultScrollTop)
const [currentScrollTop, setCurrentScrollTop] = useState(defaultScrollTop)
console.log(currentScrollTop)
useEffect(() => {
const handleDocumentScroll = () => {
const scrollTop = getScrollPosition()
setCurrentScrollTop(scrollTop)
console.log(currentScrollTop)
previousScrollTop.current = scrollTop
console.log(previousScrollTop.current)
}
const handleDocumentScrollThrottled = throttle(handleDocumentScroll, timeout)
document.addEventListener('scroll', handleDocumentScrollThrottled)
return () => {
document.removeEventListener('scroll', handleDocumentScrollThrottled)
}
}, [timeout])
return {
scrollTop: currentScrollTop,
previousScrollTop: previousScrollTop.current,
time: timeout,
}
}
UPDATE*
I've managed to work a fix with a setTimeout inside my handleDocumentScroll... Should delay between the values...
But still unsure why this worked before as it was and not now....
const handleDocumentScroll = () => {
const scrollTop = getScrollPosition()
setCurrentScrollTop(scrollTop)
setTimeout(function () {
previousScrollTop.current = scrollTop
}, 200); // update previous 200ms after event fires
}
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.
const Navbar = () => {
const prevScrollY = React.useRef<number>();
const [isHidden, setIsHidden] = React.useState(false);
React.useEffect(() => {
const onScroll = () => {
const scrolledDown = window.scrollY > prevScrollY.current!;
console.log(`is hidden ${isHidden}`);
if (scrolledDown && !isHidden) {
setIsHidden(true);
console.log(`set hidden true`);
} else if (!scrolledDown && isHidden) {
console.log(`set hidden false. THIS NEVER HAPPENS`);
setIsHidden(false);
}
prevScrollY.current = window.scrollY;
};
console.log("adding listener");
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
}, []);
return isHidden ? null : <div>navbar</div>;
};
Full example
The console.log(`is hidden ${isHidden}`); always prints false, and the setIsHidden(true) always gets triggered but never seems to change the state. Why? Basically the isHidden is never setto false, except after the useState initialization.
Basically what happens is that your useEffect runs only twice on mount and on unmount (and that's apparently intentional), however the unwanted side-effect of this is that the value of isHidden that you're checking against in the onScroll method gets closured at it's initial value (which is false) - forever (until the unmount that is).
You could use functional form of the setter, where it receives the actual value of the state and put all the branching logic inside it. Something like:
setIsHidden(isHidden => { // <- this will be the proper one
const scrolledDown = window.scrollY > prevScrollY.current!;
console.log(`is hidden ${isHidden}`);
if (scrolledDown && !isHidden) {
console.log(`set hidden true`);
return true;
} else if (!scrolledDown && isHidden) {
console.log(`set hidden false. THIS NEVER HAPPENS`);
return false;
} else {
// ...