I'm trying to make one of those custom useKeyPress hooks, and I want my keydown handler to fire on every individual button press, i.e. holding down a key won't trigger it a million times. I'm trying to do this by having a flag that indicates whether to call the handler, callHandler - it should only be set to true when the target key is released/the component just mounted. Below is a simplified version of what I'm trying to do:
const useKeyPress = (target)=>{
const [callHandler, setCallHandler] = useState(true);
const keyDown = (e) =>{
if(e.key === target && callHandler){
setCallHandler(false); //This line never seems to work properly???
console.log(callHandler, setCallHandler);
//call the keyDown handler
}
}
const keyUp = (e)=>{
if(e.key === target)
setCallHandler(true);
}
useEffect(()=>{
document.addEventListener('keydown', keyDown);
document.addEventListener('keyup' keyUp);
return ()=>{
document.removeEventListener('keydown', keyDown);
document.removeEventListener('keyup', keyUp);
}
},[]);
}
useKeyPress('a');
My issue is that callHandler is never set to false. That console log immediately after setCallHandler just spams the debug screen with (true, ...) when I hold down the 'a' key, meaning that despite the code block running callHandler is somehow always false. Any ideas as to why this isn't working?
Im not sure if i get it right , anyway i made this code hope helps u a bit.
EDIT
Change it to custom hook example, maybe useful for someone in future.
const useKeyEvent = (target) =>{
const [keyTarget,setKeyTarget] = React.useState(target);
const [playedKey,setPlayedKey] = React.useState(null);
const [keyDone,setKeyDone] = React.useState(false);
const keyUp = (e) => {
setPlayedKey(null);
setKeyDone(false)
};
const keyDown = (e) => {
const key = e.key;
if (key !== playedKey) {
if(key === keyTarget){
setKeyDone(true);
}
setPlayedKey(key);
}
};
React.useEffect(() => {
document.addEventListener("keydown", keyDown);
document.addEventListener("keyup", keyUp);
return () => {
document.removeEventListener("keydown", keyDown);
document.removeEventListener("keyup", keyUp);
};
}, [keyUp, keyDown,keyDone]);
return [playedKey,keyTarget,setKeyTarget,keyDone];
}
const App = () => {
const [playedKey,keyTarget,setKeyTarget,keyDone] = useKeyEvent("a");
return (
<div>
<h3 style={{color:keyDone?"green":"black"}}>Press {keyTarget} key ! Key : {playedKey}</h3>
<input type="text" onChange={(e)=>setKeyTarget(e.currentTarget.value)} maxLength="1" value={keyTarget}/>
</div>
)
}
ReactDOM.render(<App/>, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Related
I am trying to build a timer with three buttons, a start, stop, where it stops on the current integer, and a reset. I have added my code below which results in the following issues.
I thought my stop function would stop the timer from decrementing but it continues to do so. Also, when logging my timer state in the console, you can see it does not update in the console even though it is updating in the DOM. Why is this?
Thank you for any insight at all.
import React from 'react';
import './style.css';
export default function App() {
const [timer, setTimer] = React.useState(50);
const reset = () => {
setTimer(50);
};
const start = () => {
setTimer((prev) => prev - 1);
};
// const interval = setInterval(() => {
// console.log(updated)
// //start() }, 1000)
// }
const interval = () => {
setInterval(() => {
console.log('updated');
console.log(timer);
start();
}, 1000);
};
const stop = () => {
clearInterval(start);
};
return (
<div>
<h1>{timer}</h1>
<button onClick={interval}>start</button>
<button onClick={stop}>stop</button>
<button onClick={reset}>reset</button>
</div>
);
}`
You have a small problem with assigning the actual value for the interval.
Here is how it should be in usage
const interval = setInterval(() => {})
clearInterval(interval)
For your code change, you can create a ref to keep the interval variable and use it to clean up the interval later.
function App() {
const [timer, setTimer] = React.useState(5);
const intervalRef = React.useRef(); //create a ref for interval
const reset = () => {
setTimer(5);
};
const start = () => {
setTimer((prev) => {
if(prev === 0) {
stop();
return 0;
}
return prev - 1;
});
};
// const interval = setInterval(() => {
// console.log(updated)
// //start() }, 1000)
// }
const interval = () => {
//assign interval ref here
intervalRef.current = setInterval(() => {
start();
}, 1000);
};
const stop = () => {
//clear the interval ref
clearInterval(intervalRef.current);
};
return (
<div>
<h1>{timer}</h1>
<button onClick={interval}>start</button>
<button onClick={stop}>stop</button>
<button onClick={reset}>reset</button>
</div>
);
}
ReactDOM.render(
<App/>,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
clearTimeout or clearInterval each take a token, which was returned by a previous call to setTimeout or setInterval. So you'll need to store that token:
const id = setInterval(() => console.log("triggered"), 1000);
// ...
clearInterval(id)
Also, you should be careful about what happens if App is re-rendered, so you should probably put the set/clear logic inside useEffect so you can cleanup your interval.
Also also, although you didn't ask, your console.log(timer) isn't going to work, and will always print 50. The timer variable inside that callback is captured once, and is never updated because that callback is just inside the setInterval now. You'll need to clear and reset your interval with an updated callback function every time App re-renders, or use a ref that you keep updated, which is a pain.
I would recommend borrowing this custom hook that considers all of these things for you: https://usehooks-ts.com/react-hook/use-interval
Then your App component could become extremely simple, but still be robust:
const { useEffect, useRef, useState, useLayoutEffect } = React;
// https://usehooks-ts.com/react-hook/use-interval
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useLayoutEffect(() => {
savedCallback.current = callback;
}, [callback])
useEffect(() => {
if (!delay && delay !== 0) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
function App() {
const [timer, setTimer] = useState(50);
const [running, setRunning] = useState(false);
useInterval(() => setTimer(t => t - 1), running ? 1000 : null);
const start = () => setRunning(true);
const stop = () => setRunning(false);
const reset = () => { setTimer(50); };
return (
<div>
<h1>{timer}</h1><button onClick={start}>start</button>
<button onClick={stop}> stop </button>
<button onClick={reset}> reset </button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("react"));
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
I think you should not wrap your setTimeout into a callable. Then you lose the ability to start and stop it because the variable does not reference the interval but the callable that wraps the interval.
Take a look at this guide: https://www.w3schools.com/jsref/met_win_clearinterval.asp
I do apologise in advance as I'm still new to ReactJS. I'm fooling around with eventListeners on keyboard press's and I've come across an example of a custom hook being used on handling keypress's. I've tried refactoring the code to suite my needs but I'm unable to do so. Currently I have the eventListener assigned on checking for the "Enter" key being pressed, but I'd like to change it to the use of "Ctrl+S" being pressed instead, how would I go about doing that and if a short explanation could be provided, I'd really appreciate that. I understand that keydown would be suited best, But I'm unable to get it working.
here is what I have:
function useKey(key, cb){
const callback = useRef(cb);
useEffect(() => {
callback.current = cb;
})
useEffect(() => {
function handle(event){
if(event.code === key){
callback.current(event);
}
}
document.addEventListener('keypress',handle);
return () => document.removeEventListener("keypress",handle)
},[key])
}
function App() {
const handleSubmit = () => {
alert('submitted')
}
useKey("Enter", handleSubmit)
render (
<div>....</div>
)
}
To capture ctrl+S what you need is e.ctrlKey along with `e.code === 's'. So here is a little modification:
function useKey(key, cb){
const callback = useRef(cb);
useEffect(() => {
callback.current = cb;
})
useEffect(() => {
function handle(event){
if(event.code === key){
callback.current(event);
} else if (key === 'ctrls' && event.key === 's' && event.ctrlKey) {
callback.current(event);
}
}
document.addEventListener('keydown',handle);
return () => document.removeEventListener("keydown",handle)
},[key])
}
function App() {
const handleSubmit = () => {
alert('submitted')
}
useKey("Enter", handleSubmit)
useKey('ctrls', () => console.log('Ctrl+S fired!'));
render (
<div>....</div>
)
}
I've been stuck on this error for a long time, so I would appreciate some help. Here is a minimally reproducible exmaple:
import "./App.css";
import React, { useState } from "react";
import $ from "jquery";
function App() {
const [value, setValue] = useState("");
const focusHandler = () => {
$("input").on("keypress", (e) => {
let copy = value;
if (e.key === ".") {
e.preventDefault();
copy += " ";
setValue(copy);
}
});
};
const blurHandler = (event) => {
$("input").off("keypress");
setValue(event.target.value);
};
const changeHandler = (event) => {
setValue(event.target.value);
};
return (
<div>
<input
value={value}
onFocus={focusHandler}
onBlur={blurHandler}
onChange={changeHandler}
/>
</div>
);
}
export default App;
On input focus, I'm adding an event listener to look for a . keypress and append a tab (4 spaces) to the input if it is pressed. But when I press . multiple times, the input gets stuck at the first tab, and doesn't move any further (e.g. input permanenetly shows 4 spaces). Using console.log shows me that the value state doesn't seem to be updating in focusHandler and reverts to the original value ("").
An important note is that switching to a class-based component with this.state makes it work. Any insight as to why this is happening?
As mentioned in the comments, jQuery is the wrong tool for the job. Bringing in jQuery is the same as calling DOM methods directly: it's circumventing React and adding additional listeners on top of the ones React already gives you. You can expect misbehavior if you're setting state from multiple handlers unrelated to React. Once you're in React, use the tools it gives you (refs, effects, handlers) to solve the problem.
Worst case scenario is when an approach appears to work, then fails in production, on other people's machines/browsers, when refactoring from classes to hooks or vice-versa, in different versions of React, or for 1 out of 1000 users. Staying well within the API React gives you better guarantees that your app will behave correctly.
Controlled component
For manipulating the input value, you can use both onKeyDown and onChange listeners. onKeyDown fires first. Calling event.preventDefault() inside of onKeyDown will block the change event and ensure only one call to setState for the controlled input value occurs per keystroke.
The problem with this the input cursor moves to the end after the component updates (see relevant GitHub issue). One way to deal with this is to manually adjust the cursor position when you've made an invasive change to the string by adding state to keep track of the cursor and using a ref and useEffect to set selectionStart and selectionEnd properties on the input element.
This causes a brief blinking effect due to asynchrony after the render, so this isn't a great solution. If you're always appending to the end of the value, you assume the user won't move the cursor as other answers do, or you want the cursor to finish at the end, then this is a non-issue, but this assumption doesn't hold in the general case.
One solution is to use useLayoutEffect which runs synchronously before the repaint, eliminating the blink.
With useEffect:
const {useEffect, useRef, useState} = React;
const App = () => {
const [value, setValue] = useState("");
const [cursor, setCursor] = useState(-1);
const inputRef = useRef();
const pad = ". ";
const onKeyDown = event => {
if (event.code === "Period") {
event.preventDefault();
const {selectionStart: start} = event.target;
const {selectionEnd: end} = event.target;
const v = value.slice(0, start) + pad + value.slice(end);
setValue(v);
setCursor(start + pad.length);
}
};
const onChange = event => {
setValue(event.target.value);
setCursor(-1);
};
useEffect(() => {
if (cursor >= 0) {
inputRef.current.selectionStart = cursor;
inputRef.current.selectionEnd = cursor;
}
}, [cursor]);
return (
<div>
<p>press `.` to add 4 spaces:</p>
<input
ref={inputRef}
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
/>
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
input {
width: 100%;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
With useLayoutEffect:
const {useLayoutEffect, useRef, useState} = React;
const App = () => {
const [value, setValue] = useState("");
const [cursor, setCursor] = useState(-1);
const inputRef = useRef();
const pad = ". ";
const onKeyDown = event => {
if (event.code === "Period") {
event.preventDefault();
const {selectionStart: start} = event.target;
const {selectionEnd: end} = event.target;
const v = value.slice(0, start) + pad + value.slice(end);
setValue(v);
setCursor(start + pad.length);
}
};
const onChange = event => {
setValue(event.target.value);
setCursor(-1);
};
useLayoutEffect(() => {
if (cursor >= 0) {
inputRef.current.selectionStart = cursor;
inputRef.current.selectionEnd = cursor;
}
}, [cursor]);
return (
<div>
<p>press `.` to add 4 spaces:</p>
<input
ref={inputRef}
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
/>
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
input {
width: 100%;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
Uncontrolled component
Here's another attempt using an uncontrolled component. This doesn't have the blinking problem because the DOM element's .value property is synchronously set at the same time as the .selectionStart property and is rendered in the same repaint.
const App = () => {
const pad = ". ";
const onKeyDown = event => {
if (event.code === "Period") {
event.preventDefault();
const {target} = event;
const {
value, selectionStart: start, selectionEnd: end,
} = target;
target.value = value.slice(0, start) +
pad + value.slice(end);
target.selectionStart = start + pad.length;
target.selectionEnd = start + pad.length;
}
};
return (
<div>
<p>press `.` to add 4 spaces:</p>
<input
onKeyDown={onKeyDown}
/>
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
input {
width: 100%;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
Don't mix direct DOM manipulation, whether that's vanilla JavaScript or jQuery, with React. There is no need to add an event handler with jQuery here, because your methods are already event handlers. Just use them directly:
const focusHandler = (e) => {
// handle the event here!
}
My solution:
const changeHandler = (event) => {
const key = event.nativeEvent.data;
if (key === ".") {
event.preventDefault();
const initialValue = event.target.value.split(".")[0];
console.log(initialValue);
setValue(initialValue + " ");
} else {
setValue(event.target.value);
}
};
I have code using react hooks that is fetching some data from an API after a search value has been entered.
I am trying to activate a Timeout that will stop in case the value of const [searchResults, setSearchResults] = useState([]);changes.
I am not able to find out the way to do it, I can activate it when the use effect for fetching the information is activated, but I can not deactivate it in case new information is found.
In a nutshell:
I would like to know how to clearTimeout in case searchResults has value.
My code looks like:
useEffect(() => {
setIsLoading(true);
ProductsApi.getSearchProducts(query,setSearchResults, setIsLoading);
setTimeout(() => {
setError(false)
}, 2000);
}, [query]);
useEffect(() => {
clearTimeout(timer);
}, [searchResults]);
A generic answer to your problem
const MyComponent = () => {
const [shouldRun, setShouldRun] = useState(true);
useEffect(() => {
const TIMEOUT_DURATION = 10000;
const myTimeoutCallback = () => {
if (!shouldRun) return;
// Add your logic here
console.log("My callback was invoked after a delay.");
};
const timeoutId = setTimeout(myTimeoutCallback, TIMEOUT_DURATION);
return () => clearTimeout(timeoutId);
}, [shouldRun]);
};
You can use shouldRun as a condition if the callback function should be invoked or not.
To clear the time out you can do this: const timer = setTimeout(() => {.
However with your code, timer will be out of scope when you call clearTimeout(timer).
You should adjust your code so that timer is in scope allowing access to the variable timer.
Use setInterval
function DemoApp() {
React.useEffect(() => {
const timer = window.setInterval(() => {
console.log('2 seconds has passed');
}, 2000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>
My Demo Timer | Please check console.
</div>
);
}
ReactDOM.render(
<div>
<DemoApp />
</div>,
document.querySelector("#DemoApp")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="DemoApp"></div>
I tried adding the condition on mouseenter and mouseleave however the modal is not working but when I tried to create a button onClick={() => {openModal();}} the modal will show up. Can you please tell me what's wrong on my code and which part.
const openModal = event => {
if (event) event.preventDefault();
setShowModal(true);
};
const closeModal = event => {
if (event) event.preventDefault();
setShowModal(false);
};
function useHover() {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
return () => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
};
}, [ref]);
return [ref, hovered];
}
const [ref, hovered] = useHover();
<div className="hover-me" ref={ref}>hover me</div>
{hovered && (
<Modal active={showModal} closeModal={closeModal} className="dropzone-modal">
<div>content here</div>
</Modal>
)}
building on Drew Reese's answer, you can cache the node reference inside the useEffect closure itself, and it simplifies things a bit. You can read more about closures in this stackoverflow thread.
const useHover = () => {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
const el = ref.current; // cache external ref value for cleanup use
if (el) {
el.addEventListener("mouseenter", enter);
el.addEventListener("mouseleave", leave);
return () => {
el.removeEventLisener("mouseenter", enter);
el.removeEventLisener("mouseleave", leave);
};
}
}, []);
return [ref, hovered];
};
I almost gave up and passed on this but it was an interesting problem.
Issues:
The first main issue is with the useEffect hook of your useHover hook, it needs to add/remove both event listeners at the same time, when the ref's current component mounts and unmounts. The key part is the hook needs to cache the current ref within the effect hook in order for the cleanup function to correctly function.
The second issue is you aren't removing the listener in the returned effect hook cleanup function.
The third issue is that EventTarget.addEventListener() returns undefined, which is a falsey value, thus your hook never calls modalOpen or modalClose
The last issue is with the modal open/close state/callbacks being coupled to the useHover hook's implementation. (this is fine, but with this level of coupling you may as well just put the hook logic directly in the parent component, completely defeating the point of factoring it out into a reusable hook!)
Solution
Here's what I was able to get working:
const useHover = () => {
const ref = useRef();
const _ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current) {
_ref.current = ref.current; // cache external ref value for cleanup use
ref.current.addEventListener("mouseenter", enter);
ref.current.addEventListener("mouseleave", leave);
}
return () => {
if (_ref.current) {
_ref.current.removeEventLisener("mouseenter", enter);
_ref.current.removeEventLisener("mouseleave", leave);
}
};
}, []);
return [ref, hovered];
};
Note: using this with a modal appears to have interaction issues as I suspected, but perhaps your modal works better.