I'm currently in the process of implementing a ripple effect in my application. I'm using the Material UI Button (https://material-ui.com/components/buttons/) as a starting point for this.
During my own implementation I've run into some behaviour I'm unsure about. I've created a corresponding sandbox with a basic example, https://codesandbox.io/s/zealous-cloud-69uvh?file=/src/App.js.
The Container is the Parent Component and the Ripple is the Child Component. The Container has a onMouseUp and onMouseDown handler, which adds and removes the child component respectively.
Below is a small extract of the onMouseDown logic. The rippleCounter variable is a ref object that keeps a count which is used to set the key of the Ripple.
setRipples((oldRipples) => {
return [
...oldRipples,
<Ripple
key={rippleCounter.current}
timeout={500}
rippleX={rippleX}
rippleY={rippleY}
rippleSize={rippleSize}
/>
];
});
rippleCounter.current += 1;
};
I understand that useState is asynchronous, so there is no guarantee that the key set against the Ripple component will be before or after it is incremented. This is where I'm having some difficulties understanding.
Example 2 follows this asynchronous behaviour, as the Ripple component is having its key value set either before or after. This results in the ripple being messed up as the same key is implemented in some cases.
Example 1 doesn't appear to follow this behaviour. The setRipples function is always run in an synchronous fashion, with rippleCounter always being incremented after the setRipples function is run. i.e. Same key is never useed.
Example 1
State being managed and updated before child component is removed.
const Ripple = ({
in: inProp,
onExited = () => {},
timeout,
rippleSize,
rippleY,
rippleX
}) => {
const [leaving, setLeaving] = useState(false);
const style = {
width: rippleSize,
height: rippleSize,
top: -(rippleSize / 2) + rippleY,
left: -(rippleSize / 2) + rippleX
};
useLayoutEffect(() => {
if (!inProp) {
setLeaving(true);
const timeoutValue = setTimeout(onExited, timeout);
return () => {
clearTimeout(timeoutValue);
};
}
}, [timeout, inProp, onExited]);
return (
<span style={style} className="ripple">
<span className={"ripple-child" + (leaving ? " leaving" : "")}></span>
</span>
);
};
Example 2
No state being managed.
const Ripple = ({
in: inProp,
onExited = () => {},
timeout,
rippleSize,
rippleY,
rippleX
}) => {
// const [leaving, setLeaving] = useState(false);
const style = {
width: rippleSize,
height: rippleSize,
top: -(rippleSize / 2) + rippleY,
left: -(rippleSize / 2) + rippleX
};
useLayoutEffect(() => {
if (!inProp) {
// setLeaving(true);
const timeoutValue = setTimeout(onExited, timeout);
return () => {
clearTimeout(timeoutValue);
};
}
}, [timeout, inProp, onExited]);
return (
<span style={style} className="ripple">
<span className={"ripple-child" + (!inProp ? " leaving" : "")}></span>
</span>
);
};
Apologies for the poor articulation. If there is additional information I can provide, please let me know.
Related
I am getting different behaviour depending on whether I am using a boolvalue on with useState, or whether I am using a bool value inside an object with useState.
This first bit of code will show the hidden text when the button is pressed. It uses contextMenuIsOpen which is a bool directly on the state, to control the visibility of the hidden text.
const Parent = () => {
const [contextMenuState, setContextMenuState] = useState({ isOpen: false, x: 0, y: 0, clipboard:null });
const [contextMenuIsOpen, setContextMenuIsOpen] = useState(false);
const openChild = ()=>{
setContextMenuIsOpen(true);
}
return <div><h1>Hello</h1>
<button onClick={openChild}>Open Child</button>
{contextMenuIsOpen &&
<h1>hidden</h1> }
</div>
}
export default Parent;
This next bit of code uses a property on an object which is on the state. It doesn't show the hidden text when I do it this way.
const Parent = () => {
const [contextMenuState, setContextMenuState] = useState({ isOpen: false, x: 0, y: 0, clipboard:null });
const [contextMenuIsOpen, setContextMenuIsOpen] = useState(false);
const openChild = ()=>{
contextMenuState.isOpen = true;
setContextMenuState(contextMenuState);
}
return <div><h1>Hello</h1>
<button onClick={openChild}>Open Child</button>
{contextMenuState.isOpen &&
<h1>hidden</h1> }
</div>
}
export default Parent;
React checks objects for equality by checking their reference.
Simply, look at the below example.
const x = { a : 1, b : 2};
x.a = 3;
console.log(x===x);
So when you do the below,
const openChild = ()=>{
contextMenuState.isOpen = true;
setContextMenuState(contextMenuState);
}
You are not changing the reference of contextMenuState. Hence, there is no real change of state and setContextMenuState does not lead to any rerender.
Solution:
Create a new reference.
One example, is using spread operator:
const openChild = ()=>{
setContextMenuState({ ...contextMenuState , isOpen : true });
}
The problem with your second approach is that React will not identify that the value has changed.
const openChild = () => {
contextMenuState.isOpen = true;
setContextMenuState(contextMenuState);
}
In this code, you refer to the object's field, but the object reference itself does not change. React is only detecting that the contextMenuState refers to the same address as before and from its point of view nothing has changed, so there is no need to rerender anything.
If you change your code like this, a new object will be created and old contextMenuState is not equal with the new contextMenuState as Javascript has created a new object with a new address to the memory (ie. oldContextMenuState !== newContextMenuState).:
const openChild = () => {
setContextMenuState({
...contextMenuState,
isOpen: true
});
}
This way React will identify the state change and will rerender.
State is immutable in react.
you have to use setContextMenuState() to update the state value.
Because you want to update state according to the previous state, it's better to pass in an arrow function in setContextMenuState where prev is the previous state.
const openChild = () =>{
setContextMenuState((prev) => ({...prev, isOpen: true }))
}
Try change
contextMenuState.isOpen = true;
to:
setContextMenuState((i) => ({...i, isOpen: true}) )
never change state like this 'contextMenuState.isOpen = true;'
I have a working animation of an object made with the "useRef" hook. Part of the code in this animation will be repeated several times, so I moved it into a separate function, but when I try to call this function, when rendering the component, I get the error "Can't assign to property" scrollLeft "on 1: not an object" what could be the problem?
Full code on codesandbox
https://codesandbox.io/s/peaceful-silence-bm6hx?file=/src/scroll.js
import React, {useState, useEffect, useRef} from 'react'
const Scrollable = props => {
const items = props.items;
let ref = useRef()
const [state, setState] = useState({
isScrolling:false,
clientX:0,
scrollX:0
})
const [touchStart, setTouchStart] = useState(0);
let frameId;
const onMouseDown = e =>{...}
const onMouseUp = e =>{
if(ref && ref.current && !ref.current.contains(e.target)) {
return;
}
e.preventDefault()
let touchShift = touchStart - state.clientX
let rez;
let shift;
if(touchShift > 0) {
shift = 300 - touchShift
rez = state.scrollX + shift
if(rez>2100){
rez =1800
cancelAnimationFrame(frameId)
}
let speed = shift / 20
let cur = state.scrollX
frameId = requestAnimationFrame(animate)
animate(cur,speed,rez)
}
}
const animate = (cur, speed,rez) => {
frameId = requestAnimationFrame(animate)
cur = cur + speed
ref.current.scrollLeft = cur.toFixed(2)
if (Math.round(cur) === rez) {
cancelAnimationFrame(frameId)
setState({
...state,
scrollX:rez,
isScrolling:false,
})
}
}
useEffect(() =>{
document.addEventListener('mousedown',onMouseDown)
document.addEventListener('mouseup',onMouseUp)
return () => {
document.removeEventListener('mousedown',onMouseDown)
document.removeEventListener('mouseup',onMouseUp)
}
})
useEffect(() =>{
ref.current = requestAnimationFrame(animate)
return () => {
cancelAnimationFrame(ref.current)
},[]})
return (
<div className={classes.charPage}>
<div
ref={ref}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}>
</div>
</div>
)
}
export default Scrollable;
This error means you're trying to set a property on a number. In your useEffect you're doing this:
ref.current = requestAnimationFrame(animate)
requestAnimationFrame returns, according to MDN:
A long integer value, the request id, that uniquely identifies the entry in the callback list. This is a non-zero value, but you may not make any other assumptions about its value.
But you're also using the same ref for your DOM element. After your useEffect runs it will have set your ref to the rAF id which is a number causing your error when you try to set the scrollLeft property on the ref.
What you can try next to solve this is to use 2 separate refs, one for the requestAnimationFrame and one for your DOM element.
i have component with text. It changed component own state with mouse click. But i want to save possibility to select and copy in by long click. Is there way to make it ? Selection is reset after rerender component. Code for example:
const App = () => {
const [someState, setSomeState] = React.useState(0);
const clickHandler = () => {
setSomeState(someState + 1);
}
return (
<div
className="App"
onClick={clickHandler}
>
{'State ' + someState}
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
How about using onMouseDown and onMouseUp events yourself and calculate the time the user took to click instead of using onClick?
You could for example do something like this:
const App = () => {
const [someState, setSomeState] = React.useState(0);
const [timeDown, setTimeDown] = React.useState(-1);
const clickHandler = () => setSomeState(someState + 1);
const handleMouseDown = () => setTimeDown(Date.now()); // Save the time of the mousedown event
const handleMouseUp = () => {
const timeUp = Date.now();
const timeDiff = timeUp - timeDown; // Calculate the time the user took to click and hold
if (timeDiff < 1000) { // If it's shorter than 1000ms (1s) execute the normal click handler
clickHandler();
} else { // Execute some other logic, or just ignore the click
// handleLongClick();
}
};
return (
<div
className="App"
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
>
{"State " + someState}
</div>
);
};
You can find a quick codesandbox as a demo here
I'm trying to write basic hooks to get currentScroll, lastScroll, scrollSpeed while scrolling.
function useDocScroll() {
const isClient = typeof window === "object"
function getScroll() {
return isClient
? window.pageYOffset || document.documentElement.scrollTop
: undefined
}
const [docScroll, setDocScroll] = useState(getScroll)
const [lastScroll, setLastScroll] = useState(null)
const [scrollSpeed, setScrollSpeed] = useState(Math.abs(docScroll - lastScroll))
useEffect(() => {
if (!isClient) {
return false
}
function handleScroll() {
setDocScroll(getScroll())
setLastScroll(getScroll()) // <-- why is this working?
// setLastScroll(docScroll) // <-- why is this not working?
setScrollSpeed(Math.abs(docScroll - lastScroll)) // <-- why is this not working?
}
window.addEventListener("scroll", handleScroll)
}, [])
return [docScroll, lastScroll, scrollSpeed]
}
It seems like when I do setLastScroll(getScroll()), it saves the last scroll value well.
But I don't understand because when handleScroll() is firing, shouldn't getScroll() value stays the same? I don't get it why setDocScroll(getScroll()) and setLastScroll(getScroll()) have different value.
Also, I thought I could do setLastScroll(docScroll), meaning 'set lastScroll value with current docScroll value', but it just prints '0' while docScroll value changes.
Why is this? I want to understand better.
+) And I can't get scrollSpeed which is calculated by docScroll and lastScroll, but I don't know how to get those values.
I think why your code not working is because of following two reasons:
using docScroll directly after setDocScroll won't work because setState is asynchronous task. There is no guarantee that docScroll is updated before executing next statement
you are getting 0 because of scrolling happening inside some particular element (probably). Since document.documentElement points to html element and there is no scrolling inside it. So you receive 0
Solution:
You don't need multiple useStates. Since scroll events emits too frequently, i think its good idea to use useReducer to reduce number of renders. It is important understand where scrolling happening whether on root level or inside some element.
For below solution i proposed:
if scroll happening on root level (html element) no need to pass element to useDocScroll. If scroll happening inside particular element, you need to pass element reference.
const initState = {
current: 0,
last: 0,
speed: 0,
};
function reducer(state, action) {
switch (action.type) {
case "update":
return {
last: state.current,
current: action.payload,
speed: Math.abs(action.payload - state.current) || 0,
};
default:
return state;
}
}
const isClient = () => typeof window === "object";
function useDocScroll(element = document.documentElement) {
const [{ current, last, speed }, dispatch] = useReducer(reducer, initState);
function getScroll() {
return isClient() ? element.scrollTop : 0;
}
function handleScroll() {
dispatch({ type: "update", payload: getScroll() });
}
useEffect(() => {
if (!isClient()) {
return false;
}
element.addEventListener("scroll", handleScroll);
return () => element.removeEventListener("scroll", handleScroll);
}, []);
return [current, last, speed];
}
Example:
if scroll happening inside window
const {current, last, speed} = useDocScroll()
if scroll happening in particular element
const {current, last, speed} = useDocScroll(document.getElementById("main"))
It not working due to closures:
useEffect(() => {
if (!isClient) {
return false;
}
function handleScroll() {
setDocScroll(getScroll());
// On calling handleScroll the values docScroll & lastScroll
// are always will be of the first mount,
// the listener "remembers" (closures) such values and never gets updated
setLastScroll(docScroll);
setScrollSpeed(Math.abs(docScroll - lastScroll));
// v Gets updated on every call
setLastScroll(getScroll());
}
// v Here you assigning a callback which closes upon the lexical scope of
// docScroll and lastScroll
window.addEventListener('scroll', handleScroll);
}, []);
To fix it, a possible solution can be a combination of a reference (useRef) and functional setState.
For example:
setScrollSpeed(lastScroll => Math.abs(getScroll() - lastScroll))
Not sure why lastScrolled needed:
function useDocScroll() {
const [docScroll, setDocScroll] = useState(getScroll());
const lastScrollRef = useRef(null);
const [scrollSpeed, setScrollSpeed] = useState();
useEffect(() => {
if (!isClient) {
return false;
}
function handleScroll() {
const lastScroll = lastScrollRef.current;
const curr = getScroll();
setScrollSpeed(Math.abs(getScroll() - (lastScroll || 0)));
setDocScroll(curr);
// Update last
lastScrollRef.current = curr;
}
window.addEventListener('scroll', handleScroll);
}, []);
return [docScroll, lastScrollRef.current, scrollSpeed];
}
The new React Hooks feature is cool but it sometimes makes me confused. In particular, I have this code wrapped in useEffect hook:
const compA = ({ num }) => {
const [isPositive, check] = useState(false);
useEffect(() => {
if (num > 0) check(true);
}, []);
return (//...JSX);
};
The code inside the above useEffect will be executed only once. So what are the differences if I bring the code out of the useEffect, like below:
const compA = ({ num }) => {
const [isPositive, check] = useState(false);
if (num > 0) check(true);
return (//...JSX);
};
in the second case the code will be executed at every re-render.
this is a better version of the component:
const compA = ({ num }) => {
const [isPositive, check] = useState(false);
useEffect(() => {
if (num > 0) check(true);
}, [num]);
return (//...JSX);
};
In this case the effect (which depends heavily on num) is used only when the num prop has changed.
for reference:
https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
Anyway, in my opinion using a side effect in this very simple case is overkill!
The code will run faster by checking if num > 0 at every render than checking first if num changed and then if it's > 0..
So you should probably just avoid useEffect and stick to your second piece of code