I Want to scroll to top only if the state is true:
// switch topBar (show / hide )
const [topBarOpen, setTopBarOpen] = useState(false)
const handletopBar = () => {
setTopBarOpen(prev => !prev)
}
// this useEffect always scroll to top doesn't matter if is the the true or false when I click the "Switch" button
useEffect(() => {
window.scroll({
top: 0,
behavior: 'smooth'
})
}, [handletopBar])
I'm using a switch buttom whit props "handletopBar":
<SwitchButtom handletopBar={handletopBar} />
The dependency in the useEffect should be topBarOpen. Check if it's true using a simple if statement.
useEffect(() => {
if(topBarOpen) {
window.scroll({
top: 0,
behavior: 'smooth'
})
}
}, [topBarOpen])
Related
This ugly code works. Every second viewportHeight is set to the value of window.visualViewport.height
const [viewportHeight, setViewportHeight] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setViewportHeight(window.visualViewport.height);
}, 1000);
}, []);
However this doesn't work. viewportHeight is set on page load but not when the height changes.
React.useEffect(() => {
setViewportHeight(window.visualViewport.height);
}, [window.visualViewport.height]);
Additional context: I need the page's height in state and I need the virtual keyboard's height to be subtracted from this on Mobile iOS.
You can only use state variables managed by React as dependencies - so a change in window.visualViewport.height will not trigger your effect.
You can instead create a div that spans the whole screen space and use a resize observer to trigger effects when its size changes:
import React from "react";
import useResizeObserver from "use-resize-observer";
const App = () => {
const { ref, width = 0, height = 0 } = useResizeObserver();
const [viewportHeight, setViewportHeight] = React.useState(height);
React.useEffect(() => {
setViewportHeight(window.visualViewport.height);
}, [height]);
return (
<div ref={ref} style={{ width: "100vw", height: "100vh" }}>
// ...
</div>
);
};
This custom hook works:
function useVisualViewportHeight() {
const [viewportHeight, setViewportHeight] = useState(undefined);
useEffect(() => {
function handleResize() {
setViewportHeight(window.visualViewport.height);
}
window.visualViewport.addEventListener('resize', handleResize);
handleResize();
return () => window.visualViewport.removeEventListener('resize', handleResize);
}, []);
return viewportHeight;
}
In my app there is a navbar that pops down after the user scrolled to a certain point. I use two separate navbars and define the current scroll position like this:
const newNavbar = () => {
if (window !== undefined) {
let posHeight_2 = window.scrollY;
posHeight_2 > 112 ? setNewNav(!newNav) : setNewNav(newNav)
}
};
const stickNavbar = () => {
if (window !== undefined) {
let windowHeight = window.scrollY;
windowHeight > 150 ? setSticky({ position: "fixed", top: "0", marginTop:"0", transition: "top 1s"}) : setSticky({});
}
};
const scrollPos = () => {
if (window !== undefined) {
let posHeight = window.scrollY;
posHeight > 112 ? setScroll(posHeight) : setScroll(0)
}
};
Current states are managed by useState and given to a class, which is triggered by the changing scroll position:
const [scroll, setScroll] = useState(0);
const [newNav, setNewNav] = useState (false)
const [sticky, setSticky] = useState({});
const navClass = newNav ? 'menu-2 show' : 'menu-2'
<Navbar className={navClass}>
//
</Navbar>
finally UseEffect to make use of the states:
React.useEffect(() => {
window.addEventListener('scroll', stickNavbar);
return () => window.removeEventListener('scroll', stickNavbar);
}, []);
React.useEffect(() => {
window.addEventListener('scroll', scrollPos);
return () => window.removeEventListener('scroll', stickNavbar);
}, []);
React.useEffect(() => {
window.addEventListener('scroll', newNavbar);
return () => window.removeEventListener('scroll', newNavbar);
}, []);
However my cleanup functions are not working, I get the error Warning: Can't perform a React state update on an unmounted component.
Your second useEffect contains a copy/paste error.
It should remove scrollPos (since that's what you bound), not stickNavbar.
Because of this scrollPos listener is not removed, which causes an error on the next scroll event, as the bound function no longer exists after the component is removed from DOM.
I have a button which closes a navigation. This button follows the mouse. Everything is working, but I have a depricationwarning, which I wanna get rid of, but don't know exactly how. (I only know that useEffect is playing a certain role:
Here is the class:
import React from "react"
class NavigationCloseMouseButton extends React.Component {
static defaultProps = {
visible: true,
offsetX: 0,
offsetY: 0,
}
state = {
xPosition: 0,
yPosition: 0,
mouseMoved: false,
listenerActive: false,
}
componentDidMount() {
this.addListener()
}
componentDidUpdate() {
this.updateListener()
}
componentWillUnmount() {
this.removeListener()
}
getTooltipPosition = ({ clientX: xPosition, clientY: yPosition }) => {
this.setState({
xPosition,
yPosition,
mouseMoved: true,
})
}
addListener = () => {
window.addEventListener("mousemove", this.getTooltipPosition)
this.setState({ listenerActive: true })
}
removeListener = () => {
window.removeEventListener("mousemove", this.getTooltipPosition)
this.setState({ listenerActive: false })
}
updateListener = () => {
if (!this.state.listenerActive && this.props.visible) {
this.addListener()
}
if (this.state.listenerActive && !this.props.visible) {
this.removeListener()
}
}
render() {
return (
<div
onClick={this.props.toggleNavigation}
className="tooltip color-bg"
style={{
display:
this.props.visible && this.state.mouseMoved ? "block" : "none",
opacity: this.props.visible && this.state.mouseMoved ? "1" : "0",
top: this.state.yPosition + this.props.offsetY,
left: this.state.xPosition + this.props.offsetX,
}}
>
Close Menu
</div>
)
}
}
export default NavigationCloseMouseButton
And this is what I've so far, but results with errors:
ReferenceError: getTooltipPosition is not defined
import React, { useState, useEffect } from "react"
const NavigationCloseMouseButton = () => {
const defaults = {
visible: true,
offsetX: 0,
offsetY: 0,
}
const defaultState = {
xPosition: 0,
yPosition: 0,
mouseMoved: false,
listenerActive: false,
}
const [defaultProps, setDefaultProps] = useState(defaults)
const [state, setState] = useState(defaultState)
useEffect(() => {
// Update the document title using the browser API
addListener()
}, [])
getTooltipPosition = ({ clientX: xPosition, clientY: yPosition }) => {
setState({
xPosition,
yPosition,
mouseMoved: true,
})
}
addListener = () => {
window.addEventListener("mousemove", getTooltipPosition)
setState({ listenerActive: true })
}
removeListener = () => {
window.removeEventListener("mousemove", getTooltipPosition)
setState({ listenerActive: false })
}
updateListener = () => {
if (!state.listenerActive && props.visible) {
addListener()
}
if (state.listenerActive && !props.visible) {
removeListener()
}
}
return (
<div
onClick={props.toggleNavigation}
className="tooltip color-bg"
style={{
display: props.visible && state.mouseMoved ? "block" : "none",
opacity: props.visible && state.mouseMoved ? "1" : "0",
top: state.yPosition + props.offsetY,
left: state.xPosition + props.offsetX,
}}
>
Close Menu
</div>
)
}
export default NavigationCloseMouseButton
Setting Defaults
You can destructure individual props from the props object (the argument of the function component). While destructuring, you can use the = operator to set a default value for when this prop is not set.
const NavigationCloseMouseButton = ({ visible = true, offsetX = 0, offsetY = 0, toggleNavigation }) => {
Updating a Listener
I'm sure there a lots of great answers about this so I won't go into too much detail.
You want to handle adding and removing the listener from inside your useEffect. You should use a useEffect cleanup function for the final remove. We don't want to be adding and removing the same listener so we can memoize it with useCallback.
I'm not sure what you are trying to do with listenerActive. This could be a prop, but it also seems a bit redundant with visible. I don't know that we need this at all.
Calculating Offset
I also don't know that it makes sense to pass offsetX and offsetY as props. We need the mouse to be on top of the tooltip in order for it to be clickable. We can measure the tooltip div inside this component and deal with it that way.
// ref to DOM node for measuring
const divRef = useRef<HTMLDivElement>(null);
// can caluculate offset instead of passing in props
const offsetX = -.5 * (divRef.current?.offsetWidth || 0);
const offsetY = -.5 * (divRef.current?.offsetHeight || 0);
Animation
Setting the style property display as "block" or "none" makes it hard to do any sort of CSS transition. Instead, I recommend that you handle style switching by changing the className. You could still set display: block and display: none on those classes, but I am choosing to use transform: scale(0); instead.
Code
const NavigationCloseMouseButton = ({
visible = true,
toggleNavigation
}) => {
// state of the movement
const [state, setState] = useState({
xPosition: 0,
yPosition: 0,
mouseMoved: false
});
// memoized event listener
const getTooltipPosition = useCallback(
// plain event, not a React synthetic event
({ clientX: xPosition, clientY: yPosition }) => {
setState({
xPosition,
yPosition,
mouseMoved: true
});
},
[]
); // never re-creates
useEffect(() => {
// don't need to listen when it's not visible
if (visible) {
window.addEventListener("mousemove", getTooltipPosition);
} else {
window.removeEventListener("mousemove", getTooltipPosition);
}
// clean-up function to remove on unmount
return () => {
window.removeEventListener("mousemove", getTooltipPosition);
};
}, [visible, getTooltipPosition]); // re-run the effect if prop `visible` changes
// ref to DOM node for measuring
const divRef = useRef(null);
// can caluculate offset instead of passing in props
const offsetX = -.5 * (divRef.current?.offsetWidth || 0);
const offsetY = -.5 * (divRef.current?.offsetHeight || 0);
// don't show until after mouse is moved
const isVisible = visible && state.mouseMoved;
return (
<div
ref={divRef}
onClick={toggleNavigation}
// control most styling through className
className={`tooltip ${isVisible ? "tooltip-visible" : "tooltip-hidden"}`}
style={{
// need absolute position to use top and left
position: "absolute",
top: state.yPosition + offsetY,
left: state.xPosition + offsetX
}}
>
Close Menu
</div>
);
};
Other Uses
We can easily make this NavigationCloseMouseButton into a more flexible MovingTooltip by removing some of the hard-coded specifics.
Get the contents from props.children instead of always using "Close Menu"
Accept a className as a prop
Change the name of toggleNavigation to onClick
Code Sandbox Demo
I'm trying to implement a context menu using BlueprintJs Popover component; that uses Popper.js to position the popover, under the hood.
The problemis that: I have fixed elements and absolutely positioned elements (with transform css property set to translate3d - I believe these create new stacking contexts, possibly causing issues too) in the dom tree, above the context menu, that can not be changed. I've read somewhere in the Popper.js documentation, that I should use the fixed position strategy in this case.
Unfortunately BlueprintJs Popover does not allow me (as far as I know) to set Popper.js options, only modifiers.
So can the positioning strategy be changed with modifiers?
Here's the code and what I've tried:
import React, { useState } from 'react';
import { Popover, Position, Classes } from '#blueprintjs/core';
const getModifiers = (left, top) => {
return {
preventOverflow: { boundariesElement: 'viewport' },
computeStyle: {
// set to false to avoid using transform property to position element,
// as that clashes with other transform: translate3d styles set earlier
gpuAcceleration: false,
// I could just overwrite the computeStyles fn, and use position fixed;
// but I'd like to avoid that and let Popper.js do the coordinate arithmetics
// fn: (data) => {
// return {
// ...data,
// styles: {
// ...data.styles,
// position: 'fixed',
// left: `${left}px`,
// top: `${top}px`,
// }
// };
// },
},
// here's where I try to change the position strategy using custom modifier
changeStrategyWithModifier: {
order: 0,
enabled: true,
name: 'changeStrategyWithModifier',
phase: 'main',
fn: (data) => {
return {
...data,
instance: {
...data.instance,
options: {
...data.instance.options,
positionFixed: true, // does not seem ot have any effect
strategy: 'fixed', // does not seem ot have any effect
},
},
state: {
// reset set to true to restart process after changing strategy
...data.instance.state,
reset: true,
},
positionFixed: true, // does not seem ot have any effect
};
},
},
};
};
const ContextMenu = (props) => {
const [isOpen, setOpen] = useState(false);
const [offset, setOffset] = useState();
const portalContainer = useGetPortalContainer();
const handleCloseContextMenu = () => setOpen(false);
const handleInteraction = () => setOpen(false);
const handleOpenContextMenu = (mouseEvent) => {
mouseEvent.preventDefault();
setOffset({ left: mouseEvent.clientX, top: mouseEvent.clientY });
setOpen(true);
};
const modifiers = getModifiers(offset.left, offset.top);
return (
<>
<div className={Classes.CONTEXT_MENU_POPOVER_TARGET} style={offset}>
<Popover
isOpen={isOpen}
onInteraction={handleInteraction}
content={props.renderMenu(handleCloseContextMenu)}
target={<div />}
usePortal={true}
portalContainer={portalContainer}
position={Position.TOP_LEFT}
modifiers={modifiers}
/>
</div>
{props.renderComponent(handleOpenContextMenu)}
</>
);
};
Can't find a way to remove a hook when going to the next page section.
I've created a "useMousePosition" hook that tracks mouse position and returns mouse coordinates winch I use to transform some <div/>'s position. When scrolling down the page there's no need to transform the <div/> so I want to remove this useMousePosition hook.
useMouseHook
function useMousePosition() {
let [mousePosition, setMousePosition] = useState({
x: null,
y: null
});
function handleMouseMove(e) {
setMousePosition({
x: e.pageX,
y: e.pageY
});
}
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
return mousePosition;
}
Use hook in a component like so
let { x, y } = useMousePosition();
I need to remove a hook when user has scrolled to the next page section (Component with hook doesn't unmount)
The way I understood the question is that, you want to stop tracking the mouse movement.
If my understanding is correct, you can pass a flag to start/top tracking the mouse movement.
This demo shows that you can turn on/off the mouse tracking and
You can follow along
You can simply pass a variable, which you can check within your useEffect.
function useMousePosition(shouldTrack = true) {
let [mousePosition, setMousePosition] = useState({
x: null,
y: null
});
function handleMouseMove(e) {
setMousePosition({
x: e.pageX,
y: e.pageY
});
}
useEffect(() => {
if (!shouldTrack) return;
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [shouldTrack]);
return mousePosition;
}
function App() {
const [useMouse, setUseMouse] = useState(true);
let { x, y } = useMousePosition(useMouse);
useEffect(() => {
console.log(`x, y`, x, y);
}, [x, y]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={() => setUseMouse(_ => !_)}>
Tracking Mouse Movement is {useMouse ? "On" : "Off"}
</button>
</div>
);
}
Clicking on the button toggles the track status.
And for "removing the hook", you can't as it's embedded in your Function Component.
You can at least prevent the "side effect" from running using a condition.
⚠ Note that useEffect has a dependency as [shouldTrack].
You need to specify conditions in handleMouseMove
In the next solution, you stop render outside the pink border and remove the listener under the black line.
Note: added useCallback and dep array because of unnecessary renderings.
const isInsideBox = ({ pageX, pageY }) =>
LEFT <= pageX && pageX <= RIGHT && TOP <= pageY;
function useMousePosition() {
let [mousePosition, setMousePosition] = useState({
x: null,
y: null
});
const handleMouseMove = useCallback(
e => {
isInsideBox(e) && // Add Condition for Border
setMousePosition({
x: e.pageX,
y: e.pageY
});
e.pageY >= BOTTOM && // Add Condition for Black Line
window.removeEventListener("mousemove", handleMouseMove);
},
[setMousePosition]
);
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [handleMouseMove]);
return mousePosition;
}