I am trying to detect a key press anywhere on the page in an app built with React.
I remember it being quite easy in jQuery, but I wanted to see if it could be done in React. I didn't want to make a 'frankenstein' project i.e. have some parts in react and some parts in jQuery, if at all avoidable.
So far I have:
export default function App() {
const handleKeyPress = (event) => {
console.log(event.keyCode);
}
return (
<div
onKeyDown={handleKeyPress}
tabIndex="0"
>
<Header page_num={100}/>
</div>
);
}
But it only seems to work when a user clicks on the actual div in the page, then presses a key.
I want the key press to be detected without the user having to click anywhere on the page.
You can use useEffect hook to achieve this and adding an event listener to document:
import React, { useEffect } from "react";
export default ({ name }) => {
useEffect(() => {
function handleKeyDown(e) {
console.log(e.keyCode);
}
document.addEventListener('keydown', handleKeyDown);
// Don't forget to clean up
return function cleanup() {
document.removeEventListener('keydown', handleKeyDown);
}
}, []);
return <div>Keydown</div>;
};
Here is an example in action.
Assuming the <div> is focused via tabindex or similar, you'd be able to see see in the keydown handler that e.target would be the <div>. You can also wrap the functionality in another component and in it's keydown handler check if the keydown was executed on that element instead of outside using contains.
Here is another example that uses contains to check if the event target is within the div ref:
import React, { useEffect, useRef } from "react";
function Hello({ onKeyDown }) {
const ref = useRef();
useEffect(() => {
function handleKeyDown(e) {
// check if keydown was contained in target div
if (!ref.current || ref.current.contains(event.target)) {
return;
}
// Emit event to parent component
onKeyDown(e);
}
document.addEventListener("keydown", handleKeyDown);
return function cleanup() {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<div ref={ref} tabIndex="0">
foo bar baz
</div>
);
}
export default Hello;
Hopefully that helps!
Related
I have a situation when SVG loaded from the server after the page render and after that, I need to add certain events to its elements. Let's say after clicking on a rect it shows an alert.
import React, { useEffect, useState } from 'react';
import css from "./room-plan.module.css";
import { Button } from "../../components";
export default function RoomPlan({ svg }) {
useEffect(() => {
var elements = Array.from(document.querySelectorAll('svg rect'));
elements.forEach(function(el) {
el.addEventListener("click", alert("hello"));
})
}, [])
return (
<div>
<h2 className={css.labels}>Select desk</h2>
<div id={css.roomPlan} dangerouslySetInnerHTML={{__html: svg, }}></div>
<div className={css.bookingButtons}>
<Button>CANCEL</Button>
<Button>BOOK DESK</Button>
</div>
</div>
);
}
But it totally does not work. Just does not add any events on elements. Cuz when it looks for the required selector it does not exist yet.
The answer above is correct, but make sure to include a return function inside the useEffect for cleanup during component unmount
useEffect(() => {
var elements = Array.from(document.querySelectorAll('svg rect'));
const listener = () => alert("hello");
if(elements) {
elements.forEach(function(el) {
el.addEventListener("click", listener);
})
}
return function cleanup(){
if(elements) {
elements.forEach(function(el) {
el.removeEventListener("click", listener);
})
}
}
},[])
I'm trying to understand this hook : https://usehooks.com/useOnClickOutside/
The hook looks like this :
import { useState, useEffect, useRef } from 'react';
// Usage
function App() {
// Create a ref that we add to the element for which we want to detect outside clicks
const ref = useRef();
// State for our modal
const [isModalOpen, setModalOpen] = useState(false);
// Call hook passing in the ref and a function to call on outside click
useOnClickOutside(ref, () => setModalOpen(false));
return (
<div>
{isModalOpen ? (
<div ref={ref}>
👋 Hey, I'm a modal. Click anywhere outside of me to close.
</div>
) : (
<button onClick={() => setModalOpen(true)}>Open Modal</button>
)}
</div>
);
}
// Hook
function useOnClickOutside(ref, handler) {
useEffect(
() => {
const listener = event => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
},
// Add ref and handler to effect dependencies
// It's worth noting that because passed in handler is a new ...
// ... function on every render that will cause this effect ...
// ... callback/cleanup to run every render. It's not a big deal ...
// ... but to optimize you can wrap handler in useCallback before ...
// ... passing it into this hook.
[ref, handler]
);
}
My question is, at what point will the cleanup function in my useEffect run. I read "when it's component unmounts". But I dont exactly know what this means, what component do they mean.
at what point will the cleanup function in my useEffect run
From React Docs - When exactly does React clean up an effect?
React performs the cleanup when the component unmounts. However, as we
learned earlier, effects run for every render and not just once. This
is why React also cleans up effects from the previous render before
running the effects next time.
In short, cleanup function runs when:
Component unmounts
Before running the useEffect again
I read "when it's component unmounts". But I dont exactly know what this means, what component do they mean.
They mean the component in which you use this hook. In your case, that's the App component.
Problem: React Hook "React.useEffect" is called in function "selectmenu" which is neither a React function component or a custom React Hook function.
Target: I want to Mount Component('component DidMount/WillUnmount) (using useEffect()) only when I click on a button, rather than, mounting while the file (or whole Component) is being loaded.
Actual Goal: I want to select(or highlight) a file (custom ) on click. But when the user clicks outside the dimensions of the file (), then the selected file should get deselected (remove highlight).
export default function Academics() {
let [ ismenuselected, setmenuselection] = useState(0)
const selectmenu = () => {
console.log("Menu to Select")
React.useEffect(() => {
console.log('Component DidMount/WillUnmount')
return () => {
console.log('Component Unmounted')
}
}, [isfolderselected]);
}
return (
<div onClick={selectmenu}></div>
)
}
Note:
I've tried with the UPPER case of SelectFolder #56462812. Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
I want to try something like this. But with a click of a button (useEffect() should invoke onClick event).
I think I got what you're trying to accomplish. First, you can't define a hook inside a function. What you can do is trigger the effect callback after at least one of its dependencies change.
useEffect(() => {
// run code whenever deps change
}, [deps])
Though for this particular problem (from what I understood from your description), I'd go something like this:
export default function Academics() {
let [currentOption, setCurrentOption] = useState()
function handleSelectOption(i) {
return () => setCurrentOption(i)
}
function clearSelectedOption() {
return setCurrentOption()
}
return (options.map((option, i) =>
<button
onClick={handleSelectOption(i)}
className={i === currentOption ? 'option option--highlight' : 'option'}
onBlur={clearSelectedOption}
></button>
))
}
I have a bottomTabNavigator which has two stacknavigators. Each stacknavigator has their own respective screens within them. Whenever I use something like
navigator.navigate("Stackname" {screen:"screenname", randomProp: "seomthing")
the params are sent to the stacknavigator, and not the screen itself. I kinda got past the issue by passing in
initialParams=route.params
within the stacknavigators, but they won't refresh when I call the first block of code for a second time.
Any ideas?
Instead of:
navigator.navigate("StackName" {screen:"screenName", paramPropKey: "paramPropValue");
Use this:
navigator.navigate("screenName", {'paramPropKey': 'paramPropValue'});
In screenName:
export default ({route}) => {
useEffect(() => {
// do something
}, [route]);
};
That is because the screen is already mounted & initial params won't update. What you can do, though, is create a wrapper component enhanced with 'withNavigationFocus' that 'react-native-navigation' offers.
https://reactnavigation.org/docs/1.x/with-navigation-focus/
ComponentWithFocus
import React, {Component, useState} from 'react';
import { withNavigationFocus } from 'react-navigation';
const ComponentWithFocus = (props) => {
const {isFocused, onFocus, onBlur} = props;
const [focused, setFocused] = useState(false);
if(isFocused !== focused) {
if(isFocused) {
typeof onFocus === 'function' ? onFocus() : null;
setFocused(true)
} else {
typeof onBlur === 'function' ? onBlur() : null;
setFocused(false)
}
}
return (
props.children
)
}
export default withNavigationFocus(ComponentWithFocus);
And use it in your screen like this:
...
onFocus = () => {
//your param fetch here and data get/set
this.props.navigation.getParam('param')
//get
//set
}
...
render() {
<ComponentWithFocus onFocus={this.onFocus}>
/// Your regular view JSX
</ComponentWithFocus>
}
Note: If params aren't updated still, than you should reconsider your navigating approach. For example, there is no need to navigate from your tabBar like this:
navigator.navigate("Stackname" {screen:"screenname", randomProp: "seomthing")
You could instead do the following:
navigator.navigate("screenName", {'paramPropKey': 'paramPropValue'})
This will work because '.navigate' function finds the first available screen that matches the name and if it isn't already mounted it mounts it onto the stack (firing componentDidMount method). If the screen already exists, it just navigates to it, ignoring 'componentDidMount' but passing the 'isFocused' prop which, luckily, we hooked on to in our 'ComponentWithFocus'.
Hope this helps.
function HomeScreenComponent( {navigation} ) {
React.useEffect(() => {
navigation.addListener('focus', () => {
console.log("reloaded");
});
}, [navigation]);
export default HomeScreenComponent;
This will also listen to the focusing and execute the useEffect function when the screen navigates.
I have React component. Initially I set some localStorage in UseEffect. Moreover I add event listener. After clicking on text it changes the localStorage value but event listener does not triggering, why?
import React, { useEffect } from "react";
export default function App() {
useEffect(() => {
window.localStorage.setItem("item 1", 'val 1');
window.addEventListener('storage', () => {
alert('localstorage changed!')
})
}, []);
const getData = () => {
localStorage.setItem("item", "val chamged");
};
return (
<div className="App">
<h1 onClick={getData}>Change localstorage value</h1>
</div>
);
}
https://codesandbox.io/s/naughty-engelbart-90tkw
There are two things wrong.
change onClick={getData()} onClick={getData}
From the doc(https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event). The storage event of the Window interface fires when a storage area (localStorage or sessionStorage) has been modified in the context of another document. Note the last sentence that says it won't be fired in the same document. You can see that if you open https://codesandbox.io/s/spring-browser-89con in 2 tabs in same browser, the alert will start coming.
To update the localStorage in the same window, you need to dispatch a storage event.
I changed some of the values from the original question to update the same cookie that is initially set. Don't forget that event listeners need to be removed when the component unmounts and localStorage items should have a same site and secure attribute!
import React, { useEffect } from "react";
export default function App() {
//Set localStorage item when the component mounts and add storage event listener
useEffect(() => {
const alertMessage = () => {
alert('localStorage changed!');
}
window.localStorage.setItem("item", 'val 1', { sameSite: "strict", secure: true });
window.addEventListener('storage', alertMessage);
//Remove the event listener when the component unmounts
return () => {
window.removeEventListener("storage", alertMessage);
}
}, []);
//Update the localStorage onClick
const updateData = () => {
localStorage.setItem("item", "val changed", { sameSite: "strict", secure: true });
window.dispatchEvent(new Event("storage")); //This is the important part
};
return (
<div className="App">
<h1 onClick={updateData}>Change localStorage value</h1>
</div>
);
}
The Storage event is triggered when there is a change in the window's storage area.
Note: The storage event is only triggered when a window other than
itself makes the changes.
You can see more details and demo: storage Event
The storage event handler will only affect other windows. Whenever something changes in one window inside localStorage all the other windows are notified about it and if any action needs to be taken it can be achieved by a handler function listening to the storage event.
Try with this onclick handler
import React, { useEffect } from "react";
export default function App() {
useEffect(() => {
window.localStorage.setItem("item 1", 'val 1');
window.addEventListener('storage', () => {
alert('localstorage changed!')
})
}, []);
const getData = () => {
localStorage.setItem("item", "val chamged");
};
return (
<div className="App">
<h1 onClick={()=>getData()}>Change localstorage value</h1>
</div>
);
}
I will suggest a solution. It might be an ugly solution, but it is the only solution that I have found to achieve it from the same window. (for some reasons I need to get the change :) )
I've used the MutationObserver class, for example to get any changes on the dark mode value:
const setTheme = (value) => {
let htmlClasses = document.querySelector('html').classList
if (value == true) {
htmlClasses.add('dark');
} else {
htmlClasses.remove('dark');
}
window.localStorage.setItem('dark', value)
}
Here I am adding a class each time the toggle button has been activated, then I configure an observer on the html classes like this:
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
console.log(mutation);
if (mutation.attributeName == "class") {
// Do what you want here
}
});
});
var config = {
attributes: true
};
observer.observe(document.querySelector('html'), config);
You can see also an other example (more useful for you) that I've found on github here