I'm still getting my head around react hooks but struggling to see what I'm doing wrong here. I have a component for resizing panels, onmousedown of an edge I update a value on state then have an event handler for mousemove which uses this value however it doesn't seem to be updating after the value has changed.
Here is my code:
export default memo(() => {
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
console.log(activePoint); // is null but should be 'top|bottom|left|right'
};
const resizerMouseDown = (e, point) => {
setActivePoint(point); // setting state as 'top|bottom|left|right'
window.addEventListener('mousemove', handleResize);
window.addEventListener('mouseup', cleanup); // removed for clarity
};
return (
<div className="interfaceResizeHandler">
{resizePoints.map(point => (
<div
key={ point }
className={ `interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${ point }` }
onMouseDown={ e => resizerMouseDown(e, point) }
/>
))}
</div>
);
});
The problem is with the handleResize function, this should be using the latest version of activePoint which would be a string top|left|bottom|right but instead is null.
How to Fix a Stale useState
Currently, your issue is that you're reading a value from the past. When you define handleResize it belongs to that render, therefore, when you rerender, nothing happens to the event listener so it still reads the old value from its render.
There are a several ways to solve this. First let's look at the most simple solution.
Create your function in scope
Your event listener for the mouse down event passes the point value to your resizerMouseDown function. That value is the same value that you set your activePoint to, so you can move the definition of your handleResize function into resizerMouseDown and console.log(point). Because this solution is so simple, it cannot account for situations where you need to access your state outside of resizerMouseDown in another context.
See the in-scope function solution live on CodeSandbox.
useRef to read a future value
A more versatile solution would be to create a useRef that you update whenever activePoint changes so that you can read the current value from any stale context.
const [activePoint, _setActivePoint] = React.useState(null);
// Create a ref
const activePointRef = React.useRef(activePoint);
// And create our custom function in place of the original setActivePoint
function setActivePoint(point) {
activePointRef.current = point; // Updates the ref
_setActivePoint(point);
}
function handleResize() {
// Now you'll have access to the up-to-date activePoint when you read from activePointRef.current in a stale context
console.log(activePointRef.current);
}
function resizerMouseDown(event, point) {
/* Truncated */
}
See the useRef solution live on CodeSandbox.
Addendum
It should be noted that these are not the only ways to solve this problem, but these are my preferred methods because the logic is more clear to me despite some of the solutions being longer than other solutions offered. Please use whichever solution you and your team best understand and find to best meet your specific needs; don't forget to document what your code does though.
You have access to current state from setter function, so you could make it:
const handleResize = () => {
setActivePoint(activePoint => {
console.log(activePoint);
return activePoint;
})
};
useRef for the callback
A similar approach to Andria's can be taken by using useRef to update the event listener's callback itself instead of the useState value. This allows you to use many up-to-date useState values inside one callback with only one useRef.
If you create a ref with useRef and update its value to the handleResize callback on every render, the callback stored in the ref will always have access to up-to-date useState values, and the handleResize callback will be accessible to any stale callbacks like event handlers.
function handleResize() {
console.log(activePoint);
}
// Create the ref,
const handleResizeRef = useRef(handleResize);
// and then update it on each re-render.
handleResizeRef.current = handleResize;
// After that, you can access it via handleResizeRef.current like so
window.addEventListener("mousemove", event => handleResizeRef.current());
With this in mind, we can also abstract away the creation and updating of the ref into a custom hook.
Example
See it live on CodeSandbox.
/**
* A custom hook that creates a ref for a function, and updates it on every render.
* The new value is always the same function, but the function's context changes on every render.
*/
function useRefEventListener(fn) {
const fnRef = useRef(fn);
fnRef.current = fn;
return fnRef;
}
export default memo(() => {
const [activePoint, setActivePoint] = useState(null);
// We can use the custom hook declared above
const handleResizeRef = useRefEventListener((event) => {
// The context of this function will be up-to-date on every re-render.
console.log(activePoint);
});
function resizerMouseDown(event, point) {
setActivePoint(point);
// Here we can use the handleResizeRef in our event listener.
function handleResize(event) {
handleResizeRef.current(event);
}
window.addEventListener("mousemove", handleResize);
// cleanup removed for clarity
window.addEventListener("mouseup", cleanup);
}
return (
<div className="interfaceResizeHandler">
{resizePoints.map((point) => (
<div
key={point}
className={`interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${point}`}
onMouseDown={(event) => resizerMouseDown(event, point)}
/>
))}
</div>
);
});
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
setActivePoint(currentActivePoint => { // call set method to get the value
console.log(currentActivePoint);
return currentActivePoint; // set the same value, so nothing will change
// or a different value, depends on your use case
});
};
Just small addition to the awe ChrisBrownie55's advice.
A custom hook can be implemented to avoid duplicating this code and use this solution almost the same way as the standard useState:
// useReferredState.js
import React from "react";
export default function useReferredState(initialValue) {
const [state, setState] = React.useState(initialValue);
const reference = React.useRef(state);
const setReferredState = value => {
reference.current = value;
setState(value);
};
return [reference, setReferredState];
}
// SomeComponent.js
import React from "react";
const SomeComponent = () => {
const [someValueRef, setSomeValue] = useReferredState();
// console.log(someValueRef.current);
};
For those using typescript, you can use this function:
export const useReferredState = <T>(
initialValue: T = undefined
): [T, React.MutableRefObject<T>, React.Dispatch<T>] => {
const [state, setState] = useState<T>(initialValue);
const reference = useRef<T>(state);
const setReferredState = (value) => {
reference.current = value;
setState(value);
};
return [state, reference, setReferredState];
};
And call it like that:
const [
recordingState,
recordingStateRef,
setRecordingState,
] = useReferredState<{ test: true }>();
and when you call setRecordingState it will automatically update the ref and the state.
You can make use of the useEffect hook and initialise the event listeners every time activePoint changes. This way you can minimise the use of unnecessary refs in your code.
When you need to add event listener on component mount
Use, useEffect() hook
We need to use the useEffect to set event listener and cleanup the same.
The use effect dependency list need to have the state variables which are being used in event handler. This will make sure handler don't access any stale event.
See the following example. We have a simple count state which gets incremented when we click on given button. Keydown event listener prints the same state value. If we remove the count variable from the dependency list, our event listener will print the old value of state.
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const clickHandler = () => {
console.log({ count });
setCount(c => c + 1);
}
useEffect(() => {
document.addEventListener('keydown', normalFunction);
//Cleanup function of this hook
return () => {
document.removeEventListener('keydown', normalFunction);
}
}, [count])
return (
<div className="App">
Learn
<button onClick={clickHandler}>Click me</button>
<div>{count}</div>
</div>
);
}
export default App;
Related
I know that the Synthetic Event, instead of the native dom event, should be the way to deal with interaction in React App, but I wonder why the behavior is so bizarre. When the React Event works well, and the dom event always reset the state to initial state.
import React, { useState, useEffect } from "react";
import "./style.css";
export default function App() {
const [num, setNum] = useState(0);
function clickMe() {
setNum(num + 1);
}
useEffect(() => {
document.getElementById("app_btn").addEventListener("click", clickMe);
return () =>
document.getElementById("app_btn").removeEventListener("click", clickMe);
}, []);
return (
<div>
<h1>Hello StackBlitz!</h1>
<div>App {num}</div>
<button id="app_btn">DOM click</button> <br />
<button onClick={clickMe}>React Click</button>
</div>
);
}
The full project can be opened here https://stackblitz.com/edit/react-wqrmcu?file=src/App.js
That's happening because your native event handler still closes over the value of num that was available after first render (that value is 0). You need to add and remove native event listener at each state update for this to work like so :-
useEffect(() => {
document.getElementById("app_btn").addEventListener("click", clickMe);
return () =>
document.getElementById("app_btn").removeEventListener("click", clickMe);
}, [num]);
Another approach could be using a callback inside the state updator function as #Mahdi pointed out. This will allow you to get new state on the basis of previous state. But still the value of num being accessible to native event listener is 0. You can verify that by doing a console.log(num) inside clickMe.
function clickMe() {
setNum(prevNum => prevNum + 1); // <-- you need this
}
This takes place in a functional component:
import {useEffect} from 'react';
let [clickedOnPiece, setClickedOnPiece] = useState(false);
let [testRender, setTestRender] = useState(false);
useEffect(() => {
testRenderFunction();
}, [])
function testRenderFunction() {
let el = <div onClick={onClickHandler}>Click me</div>;
setTestRender(el);
}
function onClickHandler() {
if (clickedOnPiece) {
console.log("already clicked")
return
}
console.log(clickedOnPiece); //returns false the 1st & 2nd time.
setClickedOnPiece("clicked");
}
return (
<>
{testRender}
</>
)
When I click on div for the first time, I wait until setClickedOnPiece("clicked") successfully updates clickedOnPiece to "clicked". (I check this with React Developer Tools).
When I click div the 2nd time, it doesn't log the new change in state. It still logs clickedOnPiece as false. Why is this?
Okey this problem is because useState is asyncronus. u can read more about this useState set method not reflecting change immediately.
I think the solution is add useEffect like this.
useEffect( () => {
console.log(clickOnPiece);
}
, [clickOnPiece])
If you want to toggle the state, you could do something like this:
let [clickedOnPiece, setClickedOnPiece] = useState(false);
const onClickHandler = () => {
// set the value of clickedOnPiece to the opposite of what it was
// i.e. if it was 'true', set it to 'false'.
setClickedOnPiece(!clickedOnPiece);
console.log(clickedOnPiece);
}
// call the onClickHandler on click
<div onClick={()=>onClickHandler()}>Click me</div>
Looks like you are toggling
let [clickedOnPiece, setClickedOnPiece] = useState(false);
const onClickHandler = () => {
console.log(clickedOnPiece);
setClickedOnPiece(!clickedOnPiece);
}
console.log(clickedOnPiece);
<div onClick={onClickHandler}>Click me</div>
After setting state, don't console immediately because state is an asynchronous.
onClickHandler references the old, previous variable, clickedOnPiece. I believe this is because onClickHandler is not defined in the return statement part of the functional component which would have allowed it a new onClickHandler body to be created each time. Instead, we have the old onClickHandler continually referencing the old clickedOnPiece.
This problem is known as 'stale closures' - a concept I found discussed well at the bottom of this article
I already visited this link and tried to follow some examples: Perform debounce in React.js
A bit of context: I'm building a search box that I want to deploy on NPM. Each time the user types, a prop function onSearch is called. This to allow the programmers to fetch new data if they want.
The problem: each character typed will trigger onSearch, but that's not optimal, so I want to debounce that.
I wanted to do as one of the posts suggests:
import React, { useCallback } from "react";
import { debounce } from "lodash";
const handler = useCallback(debounce(someFunction, 2000), []);
const onChange = (event) => {
// perform any event related action here
handler();
};
My problem is that I need to pass an argument to "someFunction", and that argument is a state (a string):
const [searchString, setSearchString] = React.useState("");
After various attempts I finally found a solution. Remembering how I debounced the window resize event in the past, I followed more or less the same pattern. I did it by attaching an event listener to the window object and by adding a property to the event when dispatching it. It works, but is it a good solution? Is there a better way to achieve this?
React.useEffect( ()=> {
// This will contain the keyword searched when the event is dispatched (the value is stored in event.keyword)
// the only function dispatching the event is handleSetSearchString
// It's declared at this level so that it can be accessed from debounceDispatchToParent
let keyword = "";
// This function contains the onSearch function that will be debounced, inputDebounce is 200ms
const debounceDispatchToParent = debounce(() =>
onSearch(keyword, isCached("search-keyword-" + keyword)), inputDebounce);
// This function sets the keyword and calls debounceDispatchToParent
const eventListenerFunction = (e) => {
// the event has a property attached that contains the keyword
// store that value in keyword
keyword = e.keyword;
// call the function that will debounce onSearch
debounceDispatchToParent();
}
// Add the listener to the window object
window.addEventListener("dispatchToParent", eventListenerFunction, false);
// Clean up
return ()=> window.removeEventListener("dispacthToParent", eventListenerFunction);
}, []);
Then everytime the user types I call handleSetSearchString:
const handleSetSearchString = keyword => {
keyword = keyword.toLowerCase();
// If the string is longer than the minimum characters required to trigger a filter/search
if (keyword.length > minChars) {
// Here I create the event that contains the keyword
const event = new Event("dispatchToParent");
event.keyword = keyword;
window.dispatchEvent(event);
} else if (keyword.length === 0) {
// If the string is empty clear the results
setFilteredItems([]);
}
setSearchString(keyword);
};
Since both debounce and useCallback return a function you could just pass it directly.
const handler = useCallback(debounce(someFunction, 2000), []);
const onChange = (event) => {
// perform any event related action here
handler(argument1, argument2, ...args);
};
So I've got this hook to return the windowWidth for my App components. I'll call this Option #1.
import {useEffect, useState} from 'react';
function useWindowWidth() {
const [windowWidth,setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWindowWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowWidth;
}
export default useWindowWidth;
And right now I'm basically using it on every component that depends on the window width to render, like:
function Component(props) {
const windowWidth = useWindowWidth();
return(
// RETURN SOMETHING BASED ON WINDOW WIDTH
);
}
And since the hook has an event listener for the resize events, the component stays responsive even after window resizes.
But I'm worried that I'm attaching a new listener for every component that uses that hook and it might slow things down at some point. And I've though of other approach:
Option #2
I use the useWindowWidth() hook only one time, inside a top level component like <App/> and I'll provide the windowWidth value down the chain via context.
Like:
function App() {
const windowWidth = useWindowWidth();
return(
<WindowWidthContext.Provider value={windowWidth}>
<Rest_of_the_app/>
</WindowWidthContext.Provider>
);
}
And then, every component that needs it could get it via:
function Component() {
const windowWidth = useContext(WindowWidthContext);
return(
// SOMETHING BASED ON WINDOW WIDTH
);
}
QUESTION
Am I right in being bothered by that fact that I'm setting up multiple resize listeners with Option #1 ? Is Option #2 a good way to optmize that flow?
If your window with is used by so many components as you mentioned, you must prefer using context. As it reads below:
Context is for global scope of application.
So, #2 is perfect choice here per react.
First approach #1 might be good for components in same hierarchy but only up-to 2-3 levels.
I'm not sure if adding and removing event listeners is a more expensive operation than setting and deleting map keys but maybe the following would optimize it:
const changeTracker = (debounceTime => {
const listeners = new Map();
const add = fn => {
listeners.set(fn, fn);
return () => listeners.delete(fn);
};
let debounceTimeout;
window.addEventListener('resize', () => {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(
() => {
const width=window.innerWidth;
listeners.forEach(l => l(width))
},
debounceTime
);
});
return add;
})(200);
function useWindowWidth() {
const [windowWidth, setWindowWidth] = useState(
() => window.innerWidth
);
useEffect(
() =>//changeTracker returns a remove function
changeTracker((width) =>
setWindowWidth(width)
),
[]
);
return windowWidth;
}
As HMR said in an above thread, my solution was to use redux to hold the width value. With this strategy you only need one listener and you can restrict how often you update with whatever tool you like. You could check if the width value is within the range of a new breakpoint and only update redux when that is true. This only works if your components dont need a steady stream of the window width, in that case just debounce.
So... I was trying useEffect but I found a strange behavior.
I have a state in a dumb component.
I call useEffect and inside of it I add a new eventListener.
This event listener has to change the state given a condition.
Problem is the state never changes...
Ideas?
const componentToRender=()=>{
const [renderStatus, changeRenderStatus]=useState(false);
const [transitionStatus, changeTransitionStatus]=useState(false);
if(!renderStatus){
useEffect(()=>{
window.addEventListener("transitionend",(event)=>{
if(event.propertyName==="width"){
changeTransitionStatus(transitionStatus?false:true);
}
})
})
changeRenderStatus(true)
}
return (transitionStatus)?<div> First case </div>:<div> Second case</div>
}
there's another function with some DOM manipulation onMouseOver.
This function should change the state from the event listener but it doesn't.
you can't use hooks inside a if statement, see hooks-rules
you should return a clean up function from your useEffect hooks to remove the event listener and avoid memory leaks
you probably want the effect to run only once, so provide an empty array as second argument to useEffect (I don't think you need renderStatus)
inside the useEffect, when calling a state setter, prefer the functional form so that you always have a fresh state value.
example
const componentToRender = () => {
//const [renderStatus, changeRenderStatus] = useState(false);
const [transitionStatus, changeTransitionStatus] = useState(false);
// No condition
useEffect(() => {
const handler = (event) => {
if (event.propertyName === "width") {
//passe a function to state setter to get fresh state value
changeTransitionStatus(transitionStatus => transitionStatus ? false : true);
}
};
window.addEventListener("transitionend", handler);
// clean up
return () => window.removeEventListener("transitionend", handler);
}, []); // empty array => run only once
return (transitionStatus) ? <div> First case </div> : <div> Second case</div>
}