Usecallback fail to memorization - javascript

I have the following code where changes on the parent component cause child element re - render. Basically the Menu component should be appear by right click on top of the placeholder tag but when it appears the whole parent component flickers. I used Usecallback with no luck. I tried useMemo but it doesn't accept any arguments. Since my callback functions are firing as a result of events, passing target of the event is important. Therefore I should pass the argument. I appreciate any suggestion.
const [menu, setMenu] = useState({isActive: false, position: undefined});
<div className='placeholder'
onClick={clickHandler}
onContextMenu={rightClickHandler}>
{menu.isActive && <Menu menu={menu} />}
{[props.entity].map(plc => {
let Content = place[props.entity];
if(Content) {
return <Content key={Math.random()} />
}
})}
</div>
const rightClickHandler = useCallback((e) => {
e.preventDefault();
const parentPosition = e.target.getBoundingClientRect();
const position = {
left: e.clientX - parentPosition.left,
top: e.clientY - parentPosition.top
};
setMenu(
{
isActive: (menu.isActive ? menu.isActive: !menu.isActive),
position: {
left: position.left,
top: position.top
}
}
);
}, []);
const clickHandler = useCallback((e) => {
setMenu({isActive: false, module: '', position: undefined});
}, []);

You don't need useCallback for this if you use this way. I hope this solves it.
const rightClickHandler = (e) => {
e.preventDefault()
const parentPosition = e.target.getBoundingClientRect()
const position = {
left: e.clientX - parentPosition.left,
top: e.clientY - parentPosition.top,
}
setMenu((menu) => {
return {
isActive: menu.isActive ? menu.isActive : !menu.isActive,
position,
}
})
}

Remove Math.random() It will reinforce the component to re render
const [menu, setMenu] = useState({isActive: false, position: undefined});
<div className='placeholder'
onClick={clickHandler}
onContextMenu={rightClickHandler}>
{menu.isActive && <Menu menu={menu} />}
{[props.entity].map((plc, i) => {
let Content = place[props.entity];
if(Content) {
return <Content key={'somethingElse' + i} />
}
})}
</div>
const rightClickHandler = useCallback((e) => {
e.preventDefault();
const parentPosition = e.target.getBoundingClientRect();
const position = {
left: e.clientX - parentPosition.left,
top: e.clientY - parentPosition.top
};
setMenu(
{
isActive: (menu.isActive ? menu.isActive: !menu.isActive),
position: {
left: position.left,
top: position.top
}
}
);
}, []);
const clickHandler = useCallback((e) => {
setMenu
({isActive: false, module: '', position: undefined});
}, []);

Related

How to scroll as long as a button is pressed in a textarea with React.js?

I want to perform an action which is to scroll up and down as long as a button is pressed. To do that I did set a variable to false, and when I long press on a button it turns true, and it keep on executing a function as long as it's true, once the mouse is up the variable turns false and the function stops working. But it doesn't seem to work. So where do you think the problem lies? How can I scroll as long as a button is pressed with React.js?
import React, { useState } from "react";
export default function App() {
const okay = document.getElementById("text");
const [skate, setSkate] = useState(false);
const goUp = (id) => {
if (id.scrollTop !== 0) {
id.scrollTo({
top: id.scrollTop - 10,
left: 0,
behavior: "smooth"
});
}
};
const goDown = (id) => {
id.scrollTo({
top: id.scrollTop + 10,
left: 0,
behavior: "smooth"
});
};
const goUpSmooth = (id) => {
setSkate(true);
while (skate === true) {
id.scrollTo({
top: id.scrollTop + 10,
left: 0,
behavior: "smooth"
});
}
};
const goDownSmooth = (id) => {
setSkate(true);
while (skate === true) {
if (id.scrollTop !== 0) {
id.scrollTo({
top: id.scrollTop - 10,
left: 0,
behavior: "smooth"
});
}
}
};
return (
<div>
<button
onClick={() => goUp(okay)}
onMouseDown={() => goUpSmooth(okay)}
onMouseUp={() => setSkate(false)}
>
Up
</button>
<br />
<textarea id="text" placeholder="YOUR NOTES" />
<br />
<button
onClick={() => goDown(okay)}
onMouseDown={() => goDownSmooth(okay)}
onMouseUp={() => setSkate(false)}
>
Down
</button>
</div>
);
}
You can call scrollTo once with smooth behavior and then stop it on mouseup event.
Working example

React eventListener not being called during scroll(React)

I'm trying to use my useScrollPosition() hooks in my main app. It's used to adjust the container's height depending on the scroll position.
Problem: When I scroll down, the useScrollPosition() isnt being called.
Here's the working example: https://codesandbox.io/s/recursing-khorana-5yffl?file=/src/App.js
Please help me get my useScrollPosition() hook to work again. I have a feeling it is due to my fixed position styling, or the way I'm calling the eventListener.
My code:
import React, { useEffect, useState } from "react";
const useWindowSize = () => {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
window.addEventListener("resize", handleResize);
handleResize();
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowSize;
};
const useScrollPosition = () => { // error is probably here
const [position, setPosition] = useState({
x: window.scrollX,
y: window.scrollY
});
useEffect(() => {
const handleScroll = () => {
console.log("test");
setPosition({
x: window.scrollX,
y: window.scrollY
});
};
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return position;
};
const appContainerStyle = {
position: "fixed",
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "flex-end",
backgroundColor: "black",
overflowY: "scroll"
};
export default function App() {
const size = useWindowSize();
const position = useScrollPosition();
const containerStyle = {
position: "relative",
height: size.width <= 1024 && size.height <= 1366 ? "100%" : "50%",
width: "100%"
};
const contentContainerStyle = {
height: position.y <= size.height / 10 ? "90%" : "100%",
width: "100%"
};
const navContainerStyle = {
position: "fixed",
height: position.y <= size.height / 10 ? "10%" : "0%",
width: "100%",
backgroundColor: "rgba(0,0,0,.5)",
zIndex: "1",
top: "0"
};
console.log(position.y, size.height / 10);
return (
<div style={appContainerStyle}>
<div style={navContainerStyle}></div>
<div style={contentContainerStyle}>
<div style={{ ...containerStyle, backgroundColor: "red" }}></div>
<div style={{ ...containerStyle, backgroundColor: "green" }}></div>
<div style={{ ...containerStyle, backgroundColor: "blue" }}></div>
<div style={{ ...containerStyle, backgroundColor: "orange" }}></div>
<div style={{ ...containerStyle, backgroundColor: "purple" }}></div>
</div>
</div>
);
}
You are not scrolling inside the window, but inside the appContainer.
You need to add the event listener to the appContainer. That can be done with a ref:
const myScrollBox = useRef(null);
const position = useScrollPosition( myScrollBox.current );
// ...
<div id="mainContainer" style={ appContainerStyle } ref={ myScrollBox }>
Inside useScrollPosition you need to make sure the component has been rendered already:
const useScrollPosition = ( elementRef ) => { // <-- pass the element ref
const [ position, setPosition ] = useState({ x: 0, y: 0 });
const [ element, setElement ] = useState(null);
// -- set DOM element, if ref is ready and element is not set already
useEffect(() =>{
if( elementRef && !element ){
setElement( elementRef );
}
});
useEffect(() => {
if( !element ){ return; } // <-- skip if element is not ready
const handleScroll = () => {
setPosition({
x: element.scrollTop, // <-- .scrollTop instead of .scrollY
y: element.scrollLeft // <-- .scrollLeft instead of .scrollX
});
};
element.addEventListener("scroll", handleScroll);
return () => { element.removeEventListener('scroll', handleScroll); };
}, [ element ]); // <-- add element to dependencies
return position;
};

React - Render happening before state is changed

Very confused about this one. From the code, If you move the green square you will see the printed values.
I expected all of the code to run before the state is changed and the rerender however from this code example i can see the rerender happens as soon as the function to change state is called.
import React, { useState, useEffect, useRef } from "react";
import styled, { ThemeProvider, css } from "styled-components";
export default function App() {
const pos = useRef({ x: 0, y: 0 });
const offset = useRef({ x: 0, y: 0 });
const myDiv = useRef(null);
const [rerender, setRerender] = useState(0);
console.log("render");
useEffect(() => {
window.addEventListener("mousemove", doSomething);
window.addEventListener("mousedown", doSomethingAgain);
return () => {
window.removeEventListener("mousemove", doSomething);
window.removeEventListener("mousedown", doSomethingAgain);
};
}, [rerender]);
console.log("global", rerender);
function doSomething(e) {
if (e.buttons == 1) {
const newPos = {
x: e.pageX,
y: e.pageY
};
pos.current = newPos;
console.log("first", rerender);
setRerender(rerender + 1);
console.log("second", rerender);
}
}
function doSomethingAgain(e) {
const offsetX = e.pageX - myDiv.current.offsetLeft;
const offsetY = e.pageY - myDiv.current.offsetTop;
offset.current = { x: offsetX, y: offsetY };
}
return (
<div className="App">
<DIV ref={myDiv} off={offset.current} pos={pos.current}></DIV>
</div>
);
}
const DIV = styled.div`
display: inline-block;
width: 100px;
height: 100px;
background-color: green;
position: absolute;
top: ${({ pos, off }) => pos.y - off.y}px;
left: ${({ pos, off }) => pos.x - off.x}px;
`;

Crossfade between Gatsby Images

With gatsby-image, I'm swapping through some photos using setInterval() and changing the src, like so:
componentDidMount() {
this.setState({
intervalFunction: setInterval(this.imageCycle, 10000),
});
}
componentWillUnmount() {
clearInterval(this.intervalFunction);
}
imageCycle() {
let newImage = this.state.equiptmentCurrent + 1;
if (newImage >= this.state.equiptmentImages.length) {
newImage = 0;
}
this.setState(state => ({
equiptmentCurrent: newImage,
}));
}
render method:
<IMG
sizes={this.state.equiptmentImages[this.state.equiptmentCurrent]}
outerWrapperClassName="coverOuter"
position="absolute"
style={gatsbyImgStyle}
/>
is there any way to put a transition on this when the source changes?
Here's a possible approach:
Stack two tags on top of eachother via position: absolute
Style both of them with transition: opacity 1s ease-in-out;
Place a new showFront: true property on this.state.
On the componentDidMount interval hook:
Update the next images sizes (via the state obj) for the component that isn't active.
Add Opacity of 1 and 0 (respectfully) on each component depending on value of showFront. You can conditionally add a new class with something like: className={"my-image-class " + (this.state.showFront ? 'seen' : 'not-seen')} (and reversed for the bottom image). In styled-components, can do this by passing showFront as a prop.
Toggle showFront via the componentDidMount setInterval hook.
Here is my CrossFadeImage implementation. It similar to img except that it can handle the animation for you when detecting props.src changes and has extra props to customize the transition
import React from "react";
const usePrevious = <T extends any>(value: T) => {
const ref = React.useRef<T>();
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
const useRequestAnimationFrame = (): [(cb: () => void) => void, Function] => {
const handles = React.useRef<number[]>([]);
const _raf = (cb: () => void) => {
handles.current.push(requestAnimationFrame(cb));
};
const _resetRaf = () => {
handles.current.forEach((id) => cancelAnimationFrame(id));
handles.current = [];
};
return [_raf, _resetRaf];
};
type ImageProps = {
src: string;
alt?: string;
transitionDuration?: number;
curve?: string;
};
const CrossFadeImage = (props: ImageProps) => {
const { src, alt, transitionDuration = 0.35, curve = "ease" } = props;
const oldSrc = usePrevious(src);
const [topSrc, setTopSrc] = React.useState<string>(src);
const [bottomSrc, setBottomSrc] = React.useState<string>("");
const [bottomOpacity, setBottomOpacity] = React.useState(0);
const [display, setDisplay] = React.useState(false);
const [raf, resetRaf] = useRequestAnimationFrame();
React.useEffect(() => {
if (src !== oldSrc) {
resetRaf();
setTopSrc("");
setBottomSrc("");
raf(() => {
setTopSrc(src);
setBottomSrc(oldSrc!);
setBottomOpacity(99);
raf(() => {
setBottomOpacity(0);
});
});
}
});
return (
<div
className="imgContainer"
style={{
position: "relative",
height: "100%"
}}
>
{topSrc && (
<img
style={{
position: "absolute",
opacity: display ? "100%" : 0,
transition: `opacity ${transitionDuration}s ${curve}`
}}
onLoad={() => setDisplay(true)}
src={topSrc}
alt={alt}
/>
)}
{bottomSrc && (
<img
style={{
position: "absolute",
opacity: bottomOpacity + "%",
transition: `opacity ${transitionDuration}s ${curve}`
}}
src={bottomSrc}
alt={alt}
/>
)}
</div>
);
};
export default CrossFadeImage;
Live Demo

Recommended way to have drawer resizable?

I would like to have the material ui drawer's width resizable through a draggable handle.
My current approach is to have a mousevent listener on the whole app which checks if handle was pressed and updates the width according to mouse position on every mouse move.
This however requires a constant mouseevent listener on the whole app which seems to be overkill for a simple resize feature.
Are there better/ recommended ways of doing the resize?
You can use indicator dragger with mousedown on it.
Here for example
// styles
dragger: {
width: '5px',
cursor: 'ew-resize',
padding: '4px 0 0',
borderTop: '1px solid #ddd',
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
zIndex: '100',
backgroundColor: '#f4f7f9'
}
...
state = {
isResizing: false,
lastDownX: 0,
newWidth: {}
};
handleMousedown = e => {
this.setState({ isResizing: true, lastDownX: e.clientX });
};
handleMousemove = e => {
// we don't want to do anything if we aren't resizing.
if (!this.state.isResizing) {
return;
}
let offsetRight =
document.body.offsetWidth - (e.clientX - document.body.offsetLeft);
let minWidth = 50;
let maxWidth = 600;
if (offsetRight > minWidth && offsetRight < maxWidth) {
this.setState({ newWidth: { width: offsetRight } });
}
};
handleMouseup = e => {
this.setState({ isResizing: false });
};
componentDidMount() {
document.addEventListener('mousemove', e => this.handleMousemove(e));
document.addEventListener('mouseup', e => this.handleMouseup(e));
}
...
<Drawer
variant="permanent"
open
anchor={'right'}
classes={{
paper: classes.drawerPaper
}}
PaperProps={{ style: this.state.newWidth }}
>
<div
id="dragger"
onMouseDown={event => {
this.handleMousedown(event);
}}
className={classes.dragger}
/>
{drawer}
</Drawer>
The idea is, when click the dragger, it will resize width Drawer followed mouse move.
Play DEMO.
I would like to add an answer that is more up to date using React Hooks.
You can do it like this, then:
CSS:
sidebar-dragger: {
width: '5px',
cursor: 'ew-resize',
padding: '4px 0 0',
borderTop: '1px solid #ddd',
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
zIndex: '100',
backgroundColor: '#f4f7f9'
}
React (using hooks with refs and states)
let isResizing = null;
function ResizeableSidebar (props) {
const sidebarPanel = React.useRef('sidebarPanel');
const cbHandleMouseMove = React.useCallback(handleMousemove, []);
const cbHandleMouseUp = React.useCallback(handleMouseup, []);
function handleMousedown (e) {
e.stopPropagation();
e.preventDefault();
// we will only add listeners when needed, and remove them afterward
document.addEventListener('mousemove', cbHandleMouseMove);
document.addEventListener('mouseup', cbHandleMouseUp);
isResizing = true;
};
function handleMousemove (e) {
if (!isResizing) {
return;
}
let offsetRight =
document.body.offsetWidth - (e.clientX - document.body.offsetLeft);
let minWidth = 50;
if (offsetRight > minWidth) {
let curSize = offsetRight - 60;
// using a ref instead of state will be way faster
sidebarPanel.current.style.width = curSize + 'px';
}
};
function handleMouseup (e) {
if (!isResizing) {
return;
}
isResizing = false;
document.removeEventListener('mousemove', cbHandleMouseMove);
document.removeEventListener('mouseup', cbHandleMouseUp);
};
return <div className="sidebar-container">
<div
className="sidebar-dragger"
onMouseDown={handleMousedown}
/>
<div>
Your stuff goes here
</div>
</div>;
}
It might be a useResize hook with API to enable resizing and providing current width.
import { useCallback, useEffect, useState } from 'react'
type UseResizeProps = {
minWidth: number
}
type UseResizeReturn = {
width: number
enableResize: () => void
}
const useResize = ({
minWidth,
}: UseResizeProps): UseResizeReturn => {
const [isResizing, setIsResizing] = useState(false)
const [width, setWidth] = useState(minWidth)
const enableResize = useCallback(() => {
setIsResizing(true)
}, [setIsResizing])
const disableResize = useCallback(() => {
setIsResizing(false)
}, [setIsResizing])
const resize = useCallback(
(e: MouseEvent) => {
if (isResizing) {
const newWidth = e.clientX // You may want to add some offset here from props
if (newWidth >= minWidth) {
setWidth(newWidth)
}
}
},
[minWidth, isResizing, setWidth],
)
useEffect(() => {
document.addEventListener('mousemove', resize)
document.addEventListener('mouseup', disableResize)
return () => {
document.removeEventListener('mousemove', resize)
document.removeEventListener('mouseup', disableResize)
}
}, [disableResize, resize])
return { width, enableResize }
}
export default useResize
Then you could decouple resizing logic from your layout component like this:
const Layout = () => {
const { width, enableResize } = useResize(200);
return (
<Drawer
variant="permanent"
open
PaperProps={{ style: { width } }}
>
{drawer}
<div
style={{
position: absolute,
width: '2px',
top: '0',
right: '-1px',
bottom: '0',
cursor: 'col-resize'
}}
onMouseDown={enableResize}
/>
</Drawer>
)
Just use a synthetic event on your handle element. That way, you can avoid the messiness/performance costs of having a universal event listener. Something like the following:
render() {
return (
<div onMouseDown={this.yourResizeFunc}>
</div>
);
}
You can do that with css only, if that fits your need. It's the simplest solution. Look mom, no javascript.
.resizable {
height: 150px;
width: 150px;
border: 1px solid #333;
resize: horizontal;
overflow: auto;
}
<div class="resizable"></div>
Reference on MDN

Categories