React useCallback function doesn't get current context value - reactjs

I am trying to implement a use case with React Hooks and React Context API with mouse events.
I want to add mousemove event for a container. If user moves over an object (rectangle), a dispatch action is called and context value is updated. I want to achieve that the action is not dispatched repeatedly by checking context value before dispatching. The issue is that function doesn't get current context value.
This the event function useMouseEvents.js
import * as React from "react";
import { DragToCreateContext, actionTypes } from "./reducer";
export function useMouseEvents(elRef) {
const { dragToCreate, dispatchDragToCreate } = React.useContext(
DragToCreateContext
);
console.log("out of callback", dragToCreate);
const handleMouseMove = React.useCallback(
(evt) => {
if (evt.target.tagName === "P") {
console.log("inside callback", dragToCreate);
}
if (evt.target.tagName === "P" && dragToCreate.sourceNodeId === null) {
console.log("dispatch");
dispatchDragToCreate({
type: actionTypes.ACTIVATE,
sourceNodeId: 1
});
}
},
[dragToCreate]
);
React.useEffect(() => {
const el = elRef?.current;
if (el) {
el.addEventListener("mousemove", handleMouseMove);
return () => {
el.addEventListener("mousemove", handleMouseMove);
};
}
}, [elRef, handleMouseMove]);
}
codesandbox.io
If you hover over rectangle, you will see in console log:
inside callback {sourceNodeId: null}
dispatch
out of callback {sourceNodeId: 1}
inside callback {sourceNodeId: null}
dispatch
out of callback {sourceNodeId: 1}
inside callback {sourceNodeId: 1}
inside callback {sourceNodeId: null}
but it should be
inside callback {sourceNodeId: null}
dispatch
out of callback {sourceNodeId: 1}
inside callback {sourceNodeId: 1}

The behaviour that you see is because your listeners on mouseMove are removed and added whenever your context value changes. Also since your listener is recreated in useEffect it might so happen that before a new listener is attached, an old one executes and you get an old value from the closure.
To solve such scenarios, you can make use of a ref to keep track of updated context values and use that inside your listener callback. This way you will be able to avoid addition and removal of mouse event listener
import * as React from "react";
import { DragToCreateContext, actionTypes } from "./reducer";
export function useMouseEvents(elRef) {
const { dragToCreate, dispatchDragToCreate } = React.useContext(
DragToCreateContext
);
console.log("out of callback", dragToCreate);
const dragToCreateRef = React.useRef(dragToCreate);
React.useEffect(() => {
dragToCreateRef.current = dragToCreate;
}, [dragToCreate]);
const handleMouseMove = React.useCallback((evt) => {
if (evt.target.tagName === "P") {
console.log("inside callback", dragToCreateRef.current);
}
if (
evt.target.tagName === "P" &&
dragToCreateRef.current.sourceNodeId === null
) {
console.log("dispatch");
dispatchDragToCreate({
type: actionTypes.ACTIVATE,
sourceNodeId: 1
});
}
}, []);
React.useEffect(() => {
const el = elRef?.current;
if (el) {
el.addEventListener("mousemove", handleMouseMove);
return () => {
el.addEventListener("mousemove", handleMouseMove);
};
}
}, [elRef, handleMouseMove]);
}
Working codesandbox demo

I can't really find the answers I was hoping for but here's a couple things for you and maybe anyone else wanting to write an answer:
The inside callback {sourceNodeId: null} tells me that something is causing the Context to reset to it's initial values and the way you used your Provider is pretty atypical to what I usually see (changing this didn't really seem to fix anything though).
I thought maybe the useContext inside of useMouseEvents is getting just the default context, but I tried moving things around to guarentee that wasn't the case but that didn't seem to work. (Someone else might want to retry this?)
Edit: Removed this suggestion
Kinda unrelated to the issue, but you're going to want to change your useEffect too:
React.useEffect(() => {
const el = elRef?.current;
if (el) {
el.addEventListener("mousemove", handleMouseMove);
return () => {
el.removeEventListener("mousemove", handleMouseMove);
};
}

Related

Value isn't be updated async in React useState (React)

I want to change State with child elements in React. However, when I click once, it is not immediately updated. Click twice, it shows the correct answer.
How to update async?
export default function Example() {
const onClick = async () => {
console.log('a', test)
// should be 'b', but console log 'a'
}
const [test, setTest] = useState('a')
return (
<ClickExample setTest={setTest} onClick={onClick} />
)
}
export default function ClickExample() {
const next = useCallback(
(alphabet: string) => {
setTest(alphabet)
onClick()
},
[onClick, setTest],
)
return <SelectButton onClick={() => next('b')} />
}
You can receive the value to be updated as an argument from the onClick callback. It'll be something like this:
export default function Example() {
const [test, setTest] = useState('a')
const handleClick = (newValue) => {
setTest(newValue);
}
return (
<ClickExample onClick={handleClick} />
)
}
export default function ClickExample({ onClick }) {
return <SelectButton onClick={() => onClick('b')} />
}
NOTE: You should avoid using useCallback() when it is not necessary. Read more over the web but this article from Kent C. Dodds is a good start. As a rule of thumb: Never use useCallback()/useMemo() unless you REALLY want to improve performance after needing that improvement.
In the first render, the value of test is equal to'a'. So when the console.log is executed, it has already captured 'a' as the value of test state. (See closures and stale closures).
One way to fix this would be to create a handleClick function in the parent component which receives the new value of test as its input and set the state and log the new value(which will be updated in the next render) using its argument.
// ClickExample
const handleClick = (alphabet) => {
setTest(alphabet);
console.log('a', alphabet);
};
codesandbox

The Range Slider doesn't change the volume property of an audio element in React

I have trouble changing the volume of audio elements.
Everything works fine, the slider is working fine, its value is changed, the state gets changed, but the audio is awlays playing at 0.5.
I am new to React and this is a project about a small "drum machine".
Any recommendations are welcome!
This is my Parent Component:
function App() {
const [volume, setVolume] = useState(0.5);
const keyCodeArray = [81, 87, 69, 65, 83, 68, 90, 67];
useEffect(()=>{
document.addEventListener('keydown', (e)=>{handleKeyPress(e)});
}, []);
function handleKeyPress(e){
if(keyCodeArray.indexOf(e.keyCode)){
playSound(e);
}
}
function playSound(e){
let id = e.key.toUpperCase();
let sound = document.getElementById(id);
sound.volume = volume;
sound.currentTime = 0;
sound.play();
}
return (
<>
<Display />
<Pads />
<RangeSlider parentState={volume} parentStateSetter={(e)=> setVolume(Number(e))}/>
</>
);
}
This is my Child Component:
function RangeSlider(props) {
return (
<div>
<input type="range"
min='0'
max="1"
step='0.01'
value={props.parentState}
className='slider'
id="myRange"
onChange={(e)=> {
props.parentStateSetter(Number(e.target.value))
}}/>
</div>
);
}
The vloume in playSound is actually a copy of the real volue that set as a state, and will only be updated when the component rerender.
Also, the handleKeyPress added for event listener is also a copy of the function that defined inside App...
This may sound a bit complex, so, I'll try to explain with code below.
To create a function in which the volumn will change when the state changes
Wrap the function playSound with useCallback like this:
const playSound = useCallback((e) => {
let id = e.key.toUpperCase();
let sound = document.getElementById(id);
sound.volume = volume;
sound.currentTime = 0;
sound.play();
}, [volume]);
Now, the volumn inside of this playSound function will be updated while the state changes. (Actually, it's the function itself get updated, not just volumn)
Then, wrap the function handleKeyPress inside useCallback as well, since what we do above makes playSound to change with state update.
const handleKeyPress = useCallback((e) => {
if (keyCodeArray.indexOf(e.keyCode)) {
playSound(e);
}
}, [playSound]);
The second parameter [playSound] means, the function handleKeyPress will update when playSound changes.
Now, all the functions will be updated when volumn is updated, which... is still not enough though, because when doing addEventListener, the function we pass is still a copy, not the real one that changing with volumn.
So we'll need to do this:
useEffect(() => {
const func = (e) => { handleKeyPress(e) }; // removeEventListener will need to pass the exact same function fot it to work, so do this here
document.addEventListener('keydown', func);
return () => {
document.removeEventListener('keydown', func);
}
}, [handleKeyPress]);
The function useEffect returns will do a cleanup when new handleKeyPress appears.
Might fell a little confused in the beginning. That is how react state works.
Oh btw, the order of functions in the final code will need some change as well. Make sure to define before using.
const playSound = useCallback((e) => {
// ...
}, [volume]);
const handleKeyPress = useCallback((e) => {
// ...
}, [playSound]);
useEffect(() => {
// ...
}, [handleKeyPress]);

Cannot update a component while rendering a different Component - ReactJS

I know lots of developers had similar kinds of issues in the past like this. I went through most of them, but couldn't crack the issue.
I am trying to update the cart Context counter value. Following is the code(store/userCartContext.js file)
import React, { createContext, useState } from "react";
const UserCartContext = createContext({
userCartCTX: [],
userCartAddCTX: () => {},
userCartLength: 0
});
export function UserCartContextProvider(props) {
const [userCartStore, setUserCartStore] = useState([]);
const addCartProduct = (value) => {
setUserCartStore((prevState) => {
return [...prevState, value];
});
};
const userCartCounterUpdate = (id, value) => {
console.log("hello dolly");
// setTimeout(() => {
setUserCartStore((prevState) => {
return prevState.map((item) => {
if (item.id === id) {
return { ...item, productCount: value };
}
return item;
});
});
// }, 50);
};
const context = {
userCartCTX: userCartStore,
userCartAddCTX: addCartProduct,
userCartLength: userCartStore.length,
userCartCounterUpdateCTX: userCartCounterUpdate
};
return (
<UserCartContext.Provider value={context}>
{props.children}
</UserCartContext.Provider>
);
}
export default UserCartContext;
Here I have commented out the setTimeout function. If I use setTimeout, it works perfectly. But I am not sure whether it's the correct way.
In cartItemEach.js file I use the following code to update the context
const counterChangeHandler = (value) => {
let counterVal = value;
userCartBlockCTX.userCartCounterUpdateCTX(props.details.id, counterVal);
};
CodeSandBox Link: https://codesandbox.io/s/react-learnable-one-1z5td
Issue happens when I update the counter inside the CART popup. If you update the counter only once, there won't be any error. But when you change the counter more than once this error pops up inside the console. Even though this error arises, it's not affecting the overall code. The updated counter value gets stored inside the state in Context.
TIL that you cannot call a setState function from within a function passed into another setState function. Within a function passed into a setState function, you should just focus on changing that state. You can use useEffect to cause that state change to trigger another state change.
Here is one way to rewrite the Counter class to avoid the warning you're getting:
const decrementHandler = () => {
setNumber((prevState) => {
if (prevState === 0) {
return 0;
}
return prevState - 1;
});
};
const incrementHandler = () => {
setNumber((prevState) => {
return prevState + 1;
});
};
useEffect(() => {
props.onCounterChange(props.currentCounterVal);
}, [props.currentCounterVal]);
// or [props.onCounterChange, props.currentCounterVal] if onCounterChange can change
It's unclear to me whether the useEffect needs to be inside the Counter class though; you could potentially move the useEffect outside to the parent, given that both the current value and callback are provided by the parent. But that's up to you and exactly what you're trying to accomplish.

updating current time every second in react without rendering

Many of my components in a react native app require to know what the current time is every second. This way I can show updated real-time information.
I created a simple functionality to set the state with new Date(), but whenever I set the state, the component re-renders, which is a waste my case.
Here is what I have:
...
export default function App() {
const [currentDateTime, setCurrentDateTime] = useState(() => new Date().toLocaleString());
useEffect(() => {
const secondsTimer = setInterval(() => {
setCurrentDateTime(new Date().toLocaleString());
}, 1000);
return () => clearInterval(secondsTimer);
}, [setCurrentDateTime]);
console.log('RENDERING');
<Text>{currentDateTime}</Text>
...
I can see the console logs RENDERING every second.
Is there a way to avoid this rerendering and still update currentDateTime
Consider using shouldComponentUpdate lifecycle method; It's purpose is for preventing unnecessary renders. Add this method and tell your component not to update if this particular part of your state changes. As an example, you might add this shouldComponentUpdate() that rejects updates that are more than
// Example logic for only re-rendering every 5 seconds; Adapt as needed.
shouldComponentUpdate(nextProps, nextState) {
if (this.lastUpdatedTimeInSeconds+5 >= nextState.timeinseconds) {
return false;
}
this.lastUpdatedTimeInSeconds = nextState.timeinseconds
return true;
}
Further Reading: https://developmentarc.gitbooks.io/react-indepth/content/life_cycle/update/using_should_component_update.html
If I understand what you're saying, you want to update the DOM without triggering React's lifecycle. This is possible using refs (see React.useRef):
import * as React from "react";
import "./styles.css";
export default function App() {
const dateTimeRef = React.useRef<HTMLSpanElement>(null);
console.log("RENDERING");
React.useEffect(() => {
const secondsTimer = setInterval(() => {
if (dateTimeRef.current) {
dateTimeRef.current.innerText = new Date().toLocaleString()
}
}, 1000);
return () => clearInterval(secondsTimer);
}, []);
return <span ref={dateTimeRef} />;
}
See working demo - https://codesandbox.io/s/nice-snow-kt500?file=/src/App.tsx
Update 1
If you want to use a component such as Text, then the component will have to forward the ref to the dom, like here:
import * as React from "react";
import "./styles.css";
const Text = React.forwardRef<HTMLSpanElement>((props: any, ref) => {
console.log("RENDERING TEXT")
return <span ref={ref}></span>
});
export default function App() {
const dateTimeRef = React.useRef<HTMLSpanElement>(null);
console.log("RENDERING APP");
React.useEffect(() => {
const secondsTimer = setInterval(() => {
if (dateTimeRef.current) {
dateTimeRef.current.innerText = new Date().toLocaleString();
}
}, 1000);
return () => clearInterval(secondsTimer);
}, []);
return <Text ref={dateTimeRef} />;
}
See working demo - https://codesandbox.io/s/jolly-moon-9zsh2?file=/src/App.tsx
Eliya Cohen's answer was conceptually correct. To avoid re-rendering, we cannot use state with an interval. We need to reference the element. Unfortunately, I wasn't able to adopt Eliya's React code to React Native in the same manner, so I did some more digging and found docs on directly manipulating React Native components.
In short, you can manipulate built in RN components' PROPS and avoid re-rendering by not changing the state.
Since the <Text> component doesn't set its value with a prop, such as <Text text="my text" />, we are not able to use this method to update it. But what does work is updating the value of a TextInput since its set with the value prop. All we need to do to make the <TextInput> behave like a <Text> is to set its prop editable to false, and of course avoid default styling of it that would make it look like an input.
Here is my solution. If someone has a better one, please do propose it.
import React, { useEffect } from 'react';
import { TextInput } from 'react-native';
const Timer: React.FC = () => {
updateTime = (currentTime) => {
time.setNativeProps({ text: currentTime });
};
useEffect(() => {
const secondsTimer = setInterval(() => {
updateTime(new Date().toLocaleString());
}, 1000);
return () => clearInterval(secondsTimer);
}, []);
return <TextInput ref={(component) => (time = component)} editable={false} />;
};
export default Timer;
I also tried this and this is what that worked for me after a few attempts with Typescript.
const timeTextInput = useRef<TextInput>(null);
useEffect(()=>{
const timer = setInterval(() => {
timeTextInput.current?.setNativeProps({ text: new Date().toLocaleString() });
}, 1000);
return () => clearInterval(timer);
}, []);
Hope this helps someone in the future.

setInterval and React hooks produces unexpected results

I have the following component defined in my app scaffolded using create-react:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
setTimer();
return (
<div>
<div>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
And currentSecond is updated every second until it hits the props.secondsPerRep however if I try to start the setInterval from a click handler:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
return (
<div>
<div>
<button onClick={setTimer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Then currentSecond within the setInterval callback always returns to the initial value, i.e. 1.
Any help greeeeeeatly appreciated!
Your problem is this line setCurrentSecond(() => currentSecond + 1); because you are only calling setTimer once, your interval will always be closed over the initial state where currentSecond is 1.
Luckily, you can easily remedy this by accessing the actual current state via the args in the function you pass to setCurrentSecond like setCurrentSecond(actualCurrentSecond => actualCurrentSecond + 1)
Also, you want to be very careful arbitrarily defining intervals in the body of functional components like that because they won't be cleared properly, like if you were to click the button again, it would start another interval and not clear up the previous one.
I'd recommend checking out this blog post because it would answer any questions you have about intervals + hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ is a great post to look at and learn more about what's going on. The React useState hook doesn't play nice with setInterval because it only gets the value of the hook in the first render, then keeps reusing that value rather than the updated value from future renders.
In that post, Dan Abramov gives an example custom hook to make intervals work in React that you could use. That would make your code look more like this. Note that we have to change how we trigger the timer to start with another state variable.
const Play = props => {
const [currentSecond, setCurrentSecond] = React.useState(1);
const [isRunning, setIsRunning] = React.useState(false);
useInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(currentSecond + 1);
}
}, isRunning ? 1000 : null);
return (
<div>
<div>
<button onClick={() => setIsRunning(true)}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
I went ahead and put an example codepen together for your use case if you want to play around with it and see how it works.
https://codepen.io/BastionTheDev/pen/XWbvboX
That is because you're code is closing over the currentSecond value from the render before you clicked on the button. That is javascript does not know about re-renders and hooks. You do want to set this up slightly differently.
import React, { useState, useRef, useEffect } from 'react';
const Play = ({ secondsPerRep }) => {
const secondsPassed = useRef(1)
const [currentSecond, setCurrentSecond] = useState(1);
const [timerStarted, setTimerStarted] = useState(false)
useEffect(() => {
let timer;
if(timerStarted) {
timer = setInterval(() => {
if (secondsPassed.current < secondsPerRep) {
secondsPassed.current =+ 1
setCurrentSecond(secondsPassed.current)
}
}, 1000);
}
return () => void clearInterval(timer)
}, [timerStarted])
return (
<div>
<div>
<button onClick={() => setTimerStarted(!timerStarted)}>
{timerStarted ? Stop : Start}
</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Why do you need a ref and the state? If you would only have the state the cleanup method of the effect would run every time you update your state. Therefore, you don't want your state to influence your effect. You can achieve this by using the ref to count the seconds. Changes to the ref won't run the effect or clean it up.
However, you also need the state because you want your component to re-render once your condition is met. But since the updater methods for the state (i.e. setCurrentSecond) are constant they also don't influence the effect.
Last but not least I've decoupled setting up the interval from your counting logic. I've done this with an extra state that switches between true and false. So when you click your button the state switches to true, the effect is run and everything is set up. If you're components unmounts, or you stop the timer, or the secondsPerRep prop changes the old interval is cleared and a new one is set up.
Hope that helps!
Try that. The problem was that you're not using the state that is received by the setCurrentSecond function and the function setInterval don't see the state changing.
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
const [timer, setTimer] = useState();
const onClick = () => {
setTimer(setInterval(() => {
setCurrentSecond((state) => {
if (state < props.secondsPerRep) {
return state + 1;
}
return state;
});
}, 1000));
}
return (
<div>
<div>
<button onClick={onClick} disabled={timer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}

Resources