Why is React using the wrong value in useState? - reactjs

I have a React component like so:
https://codepen.io/darajava/pen/NWrdWeP?editors=0010
function TestUseState() {
const [count, setCount] = useState(0);
const buttonRef = useRef(null);
useEffect(() => {
buttonRef.current.addEventListener("click", () => {
alert(count);
});
setCount(10000);
}, []);
return (
<div ref={buttonRef}>
Click
</div>
)
}
I set up an event listener on the button and it seems to take the initial value of the state, after setting it directly afterwards. Is this expected behaviour? How can I avoid this?
The actual code in question (not valid, only relevant parts added):
const Game = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [player, setPlayer] = useState<IPlayer>();
const setPosition = (newPos) => {
console.log(player); // <--- player is undefined on mousemove
};
useEffect(() => {
canvas = canvasRef.current;
ctx = canvas.getContext('2d');
canvas.addEventListener('mousemove', (e: MouseEvent) => {
setPosition(getCursorPosition(canvas, e));
});
}, []);
return (
<canvas ref={canvasRef} styleName="canvas" />
);
}

Yes this is expected, but I can understand why it seems strange.
When you are adding the ref in the useEffect hook you are closing over the value of count and saving it for later, so when you click the sub it shows you the value when the component was initialized.
If you want to alert the actual value of count you can add onClick={()=>alert(count)} to the div, this is also more in following the declarative style of React.
You are discouraged to use refs in React because React maintains a virtual dom. You use refs when you need to access dom elements directly.
Edit: You can also use this for the mouse move event:
https://reactjs.org/docs/events.html#mouse-events
Write the handler function separately in the body of the functional component and pass it to canvas element's onMouseMove prop.

Related

react function component issue with usEffect and useState

Sometimes I have to use some native js libaray api, So I may have a component like this:
function App() {
const [state, setState] = useState(0)
useEffect(() => {
const container = document.querySelector('#container')
const h1 = document.createElement('h1')
h1.innerHTML = 'h1h1h1h1h1h1'
container.append(h1)
h1.onclick = () => {
console.log(state)
}
}, [])
return (
<div>
<button onClick={() => setState(state => state + 1)}>{state}</button>
<div id="container"></div>
</div>
)
}
Above is a simple example. I should init the lib after react is mounted, and bind some event handlers. And the problem is coming here: As the above shown, if I use useEffect() without state as the item in dependencies array, the value state in handler of onclick may never change. But if I add state to dependencies array, the effect function will execute every time once state changed. Above is a easy example, but the initialization of real library may be very expensive, so that way is out of the question.
Now I find 3 ways to reslove this, but none of them satisfy me.
Create a ref to keep state, and add a effect to change it current every time once state changed. (A extra variable and effect)
Like the first, but define a variable out of the function instead of a ref. (Some as the first)
Use class component. (Too many this)
So is there some resolutions that solve problems and makes code better?
I think you've summarised the options pretty well. There's only one option i'd like to add, which is that you could split your code up into one effect that initializes, and one effect that just changes the onclick. The initialization logic can run just once, and the onclick can run every render:
const [state, setState] = useState(0)
const h1Ref = useRef();
useEffect(() => {
const container = document.querySelector('#container')
const h1 = document.createElement('h1')
h1Ref.current = h1;
// Do expensive initialization logic here
}, [])
useEffect(() => {
// If you don't want to use a ref, you could also have the second effect query the dom to find the h1
h1ref.current.onClick = () => {
console.log(state);
}
}, [state]);
Also, you can simplify your option #1 a bit. You don't need to create a useEffect to change ref.current, you can just do that in the body of the component:
const [state, setState] = useState(0);
const ref = useRef();
ref.current = state;
useEffect(() => {
const container = document.querySelector('#container');
// ...
h1.onClick = () => {
console.log(ref.current);
}
}, []);

State not updating in SunEditor onChange event

version
next: 12.0.7
suneditor: 2.41.3
suneditor-react: 3.3.1
const SunEditor = dynamic(() => import("suneditor-react"), {
ssr: false,
});
import "suneditor/dist/css/suneditor.min.css"; // Import Sun Editor's CSS File
// states
const [toggle, setToggle] = useState<boolean>(false);
const [content, setContent] = useState<string>("");
useState(() => {
console.log(toggle, content);
}, [toggle, content]);
// render
return (
<SunEditor
onChange={(content) => {
setToggle(!toggle);
setContent(!content);
}
/>
);
When I type the console panel always shows me that only content was changed, but toggle is not changed.
I really don't know what happens, I just want to set other states inside SunEditor's onChange event, but I can't. Can anyone just explain to me how to fix this?
You can use a function inside the setToggle function to get the current state value and return the modified value for that state variable.
<SunEditor
onChange={(content) => {
setToggle((value) => !value);
setContent(content);
}
/>
Due to the async nature of setState, passing in a function into the state setter instead of a value will give you a reliable way to get the component’s state. It's the recommended approach when setting state based on previous state.

React native remove scrollY listener with useEffect hook

const [scrollY] = useState(new Animated.Value(0));
const [scrollYValue, setScrollYValue] = useState(0);
useEffect(() => {
scrollY.addListener(({ value }) => {
setScrollYValue(value);
});
return scrollY.removeAllListeners();
}, [scrollY, scrollYValue, setScrollYValue]);
The goal is to instantiate an Animated.Value called scrollY and add a listener that will set the current scroll value into state. This scroll value scrollYValue is shared among components so they can do whatever they need to do based on how far the user has scrolled.
Things work fine if I remove the return scrollY.removeAllListeners(); line, but I do actually want to remove the listener when this component unmounts.
Is this a case where my useEffect dependency array is incorrect? It seems to remove the listener right away and scrollY is just fixed at 0.
Update: the return value in useEffect should be a function:
useEffect(() => {
scrollY.addListener(({ value }) => {
setScrollYValue(value);
});
return () => scrollY.removeAllListeners();
}, [scrollY, scrollYValue, setScrollYValue]);

React Hooks addEventListener undefined

I'm new to React hooks, I'm just wondering why the addEventListener is undefined even though I used it inside the useEffect.
function useHover() {
const ref = useRef();
const [hovered, setHovered] = useState(true);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
ref.current.addEventListener('mouseenter', enter);
ref.current.addEventListener('mouseleave', leave);
return () => {
ref.current.removeEventListener('mouseenter', enter);
ref.current.removeventListener('mouseleave', leave);
};
}, [ref]);
return [ref, hovered];
}`enter code here`
You can nest those commands in the componentDidMount/componentWillMount and that should stage those listeners, but that’s not a best practice.
Here’s the docs I follow when I handle events.
url: https://reactjs.org/docs/handling-events.html
Beacause ref.current is always undefined. You need to set ref on some html element in order to avoid undefined. Imho, you'll need to have setRef function/const in your hook and than return it from the hook like return [ref, setRef, hovered]; Than, in component where you use that hook, when that component mounts set ref to some element.
Update:
Actually, all you need to do is to wrap
ref.current.addEventListener("mouseenter", enter);
ref.current.addEventListener("mouseleave", leave);
with if (ref.current) { ... }. Otherwise, you are trying to attach event listeners before html element gets referenced.
Example

How to addEventListener with React Hooks when the function needs to read state?

I use React with function components and hooks - and I am trying to add a scroll listener which modified the state basing on the current value of the state.
My current code looks like this:
const [isAboveTheFoldVisible, setAboveTheFoldVisible] = useState(true);
const scrollHandler = useCallback(
() => {
if (window.pageYOffset >= window.innerHeight) {
if (isAboveTheFoldVisible) {
setAboveTheFoldVisible(false);
}
} else if (!isAboveTheFoldVisible) {
setAboveTheFoldVisible(true);
}
},
[isAboveTheFoldVisible]
);
useEffect(() => {
window.addEventListener('scroll', scrollHandler);
return () => {
window.removeEventListener('scroll', scrollHandler);
};
}, [scrollHandler]);
However, because of the [scrollHandler] in useEffect, the listeners are removed and re-added when isAboveTheFoldVisible is being set... which is not necessary.
Possible solutions that I see:
Have two useEffects, one with adding scroll listener and setting a state like scroll position without reading anything
const [scrollPosition, setScrollPosition] = useState(0)
and then second having the scrollPosition as a [scrollPosition].
I do not really like this solution because it's not really explicit why we're setting the scrollPosition when you see the listener.
Using custom hook like useEventListener https://usehooks.com/useEventListener/
Using ref to reference the handler.
Do not read the value of isAboveTheFoldVisible and set the new state anyway. React compares the old state with the new one and if it's same it won't re-render.

Resources