React textarea callback when losing focus / onBlur - javascript

https://codesandbox.io/s/react-textarea-callback-on-blur-yoh8n?file=/src/App.tsx
Having a textarea in React I want to achieve two basic use cases:
Remove focus and reset some state when the user presses "Escape"
Execute a callback (saveToDatabase) when the user clicks outside of the textarea and it loses focus (=> onBlur)
<textarea
ref={areaRef}
value={input}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
/>
For the first use case I'm calling blur() on the target:
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Escape") {
console.log("Escape clicked");
setInput(inputForReset);
e.currentTarget.blur();
}
};
..but this also calls the onBlur handler, which I actually want to utilize for the second use case. I tried to determine if the event caller is the textarea itself via a ref, but that doesn't work:
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
console.log("blur");
/**
* Only save to database when losing focus through clicking
* outside of the text area, not for every blur event.
*/
if (areaRef.current && !areaRef.current.contains(e.currentTarget as Node)) {
saveToDatabase();
}
};
In other words: I want to save something to a database when the user is finished editing at the textarea, but I don't know how to distinguish between the blur event I triggered programmatically and the native blur event that the textarea uses when you click outside of the node.

I noticed what the error is.
The target of the blur event is itself
areaRef === event.target
So you have to implement an additional function to catch click outside the box.
import * as React from "react";
import "./styles.css";
import { useState, useRef, useEffect } from "react";
function useOutsideAlerter(
ref: React.RefObject<HTMLTextAreaElement>,
fun: () => void
) {
useEffect(() => {
function handleClickOutside(event: any) {
if (
ref.current &&
!ref.current.contains(event.target) &&
// THIS IS IMPORTANT TO CHECK
document.activeElement === ref.current
) {
fun();
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
export default function App() {
const areaRef = useRef<HTMLTextAreaElement>(null);
const [inputForReset, setInputForReset] = useState<string>("Original input");
const [input, setInput] = useState<string>(inputForReset);
const saveToDatabase = () => {
console.log("save to database");
setInputForReset(input);
alert(input);
};
// OUT SIDE CLICK
useOutsideAlerter(areaRef, saveToDatabase);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInput(e.target.value);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Escape") {
console.log("Escape clicked");
setInput(inputForReset);
e.currentTarget.blur();
e.stopPropagation();
}
};
// it doesnt work
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
/**
* Only save to database when losing focus through clicking
* outside of the text area, not for every blur event.
*/
console.log(event.target);
console.log(areaRef);
if (areaRef.current && !areaRef.current.contains(event.target)) {
saveToDatabase();
}
};
return (
<div className="App">
<textarea
ref={areaRef}
value={input}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className="area"
/>
<p>Input state: {input}</p>
</div>
);
}
Please check my sandbox

Maybe you can add a variable (or a class property, if you are using class component instead of a functional one) as a "preventsaveOnBlur" flag; something like:
let preventSaveOnBlur: boolean = false;
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Escape") {
console.log("Escape clicked");
setInput(inputForReset);
preventSaveOnBlur = true; // will prevent saving on DB - see handleBlur
e.currentTarget.blur();
}
};
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
console.log("blur");
/**
* Only save to database when losing focus through clicking
* outside of the text area, not for every blur event.
*/
if (!preventSaveOnBlur) {
saveToDatabase();
}
};
const handleFocus = (e: React.FocusEvent<HTMLTextAreaElement>) => {
preventSaveOnBlur = false;
};
and in the textarea you handle the onFocus event too:
<textarea
ref={areaRef}
value={input}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
onFocus={handleFocus} // will reset 'preventSaveOnBlur' to false every time the textarea get the focus
/>

Related

In react hooks, How can I delay onInput event till I have finished typing

I currently have an input field that is meant to trigger an api call when an 11 digit number is input. How do I delay it from making the call until all 11 digits are typed
Something simple like this should do the trick.
const callAPI = (value) =>{
if(value.length === 11){
console.log("Call API")
}
}
<input type="text" oninput="callAPI(value)"/>
If you are making a controlled form it is easy to check when you update the value.
import { useState } from "react";
export default function Form() {
const [inputValue, setInputValue] = useState("");
function handelChange(event) {
setInputValue(event.target.value);
// If the length is 11 characters do something
if (inputValue.length === 11) {
document.body.style.backgroundColor = "black";
} else {
// Otherwise do something else
document.body.style.backgroundColor = "white";
}
}
return (
<input
type="text"
onChange={(e) => {
handelChange(e);
}}
value={inputValue}
/>
);
}
You could make the component be a controlled component, and perform the check to trigger the API call in the input's change handler.
Here is a code sample.
import { useState } from 'react';
const MyComponent = () => {
const [inputValue, setInputValue] = useState(0);
const handleChange = (e) => {
setInputValue(e.target.value);
if (inputValue.length === 11) {
// code to trigger API call
}
}
return (
<input value={inputValue} onChange={(e) => handleChange(e)} />
);
}
Explanation:
A controlled component controls the value of the input element using the React state itself.
Import the state hook.
import { useState } from 'react';
Use a state hook for the input's value.
const [inputValue, setInputValue] = useState(0);
Set the input's value attribute to equal to the state.
<input value={inputValue} />
Add an onChange event handler to the input element.
<input value={inputValue} onChange={handleChange} />
Create the event handler.
const handleChange = (e) => { //code for event handler }
Whenever you type in the input field, this will trigger the onChange event and run the event handler handleChange.
In the event handler, first update the state using the user input.
setInputValue(e.target.value);
Then, check the length of the input value, and trigger the call accordingly.
if (inputValue.length === 11) { // code to trigger API call }

What is the correct way to handle a key event using useEffect() hook which on the other hand triggers local state changes?

Pretty new to React Hooks and I ran into this issue. I have a functional component that takes an input and sends it to the parent component when I hit the enter key (keycode = 13). The component looks something like this.
const SearchTermBar = (props) => {
const {resetStateSearchTerm, handlePowerToggleParent} = props;
const [inputTerm, handleInputChange] = useState('');
const inputRef = useRef();
useEffect(() => {
const keyPressEvent = (e) => {
if (e.keyCode === 13) {
resetStateSearchTerm(inputTerm);
handleInputChange('');
handlePowerToggleParent('search');
}
};
inputRef.current.addEventListener('keydown', keyPressEvent);
let parentInputRef = inputRef;
return () => {
console.log('remove event listener');
parentInputRef.current.removeEventListener('keydown', keyPressEvent);
}
}, [inputTerm, resetStateSearchTerm, handlePowerToggleParent]);
return (
<div className='SearchTermBar'>
<input
type='text'
placeholder='Enter search term here (Press return to confirm)'
className='SearchTermBar__Input'
value={inputTerm}
onChange={(e) => handleInputChange(e.target.value)}
ref={inputRef}
/>
</div>
);
The problem is that the event is registered and unregistered every time the inputTerm or props value changes. But I am not able to figure out the correct way to handle the event registration/removal (which should happen once ideally) I understand it is because of the dependency on the inputTerm but I would like to know a better solution to this problem.
You already has the input ref, you don't really need a state:
const NOP = () => {};
const DEFAULT_INPUT = "";
function SearchTermBar(props) {
const { resetStateSearchTerm = NOP, handlePowerToggleParent = NOP } = props;
const inputRef = useRef();
useEffect(() => {
const keyPressEvent = (e) => {
if (e.keyCode === 13) {
resetStateSearchTerm(inputRef.current.value);
inputRef.current.value = DEFAULT_INPUT;
handlePowerToggleParent("search");
}
};
inputRef.current.addEventListener("keydown", keyPressEvent);
let parentInputRef = inputRef;
return () => {
console.log("remove event listener");
parentInputRef.current.removeEventListener("keydown", keyPressEvent);
};
}, [resetStateSearchTerm, handlePowerToggleParent]);
return (
<input
type="text"
placeholder="Enter search term here (Press return to confirm)"
style={{ width: "50%" }}
ref={inputRef}
/>
);
}
Either way, if you want to keep the state, its value should be duplicated into a ref to fix the closure in the useEffect. It can be done by adding another useEffect which will update the mentioned ref.

How to properly clean up open menu when navigating to next page

I have created simple but working dropdown-menu. In this example menu does not contain anything but text 'menu' and have no styles.
When "open menu"-button is clicked, app shows div that contains the menu, and adds eventListener to document click events. When eventListener is added and user click anywhere in document, function checks if click was happened inside the menu, and if so, does nothing. If click was outside the menu, it removes eventHandler and closes the menu.
Is there something wrong with this aproach? The major problem with this is that if menu is open when I click any link on page, I got this nasty react warning:
index.js:1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I have added useEffect cleanup function to remove eventHandler if menu is open, but that does not help, I still got same error message.
Can you point me what I have done wrong?
const DropDown = () => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const closeMenu = (event: MouseEvent) => {
if (event.target && event.target instanceof HTMLElement && ref.current) {
if (ref.current.contains(event.target)) return;
}
document.removeEventListener('click', closeMenu);
setOpen(false);
};
const toggleMenu = () => {
if (!open) {
document.addEventListener('click', closeMenu);
setOpen(true);
} else {
document.removeEventListener('click', closeMenu);
setOpen(false);
}
};
useEffect(() => {
return () => {
if (open) {
document.removeEventListener('click', closeMenu);
setOpen(false);
}
};
}, [closeMenu]);
return (
<>
<button onClick={toggleMenu}>open menu</button>
{open && <div ref={ref}>menu</div>}
</>
);
};
export default DropDown;
I think the issue is caused by the setOpen(false); in the useEffect hook: the clean-up function is called when the component unmounts therefore it make no sense setting its state. For the same reason, the check on whether the menu is open or not is redundant; if the menu is unmounted, you can remove the event handler regardless.
Try:
const DropDown = () => {
const [open, setOpen] = useState(false);
const ref = useRef < HTMLDivElement > (null);
/*
*"useCallback()" hook will avoid unnecessary re-creating
* the function at each re-render
*/
const closeMenu = useCallback((event: MouseEvent) => {
if (event.target && event.target instanceof HTMLElement && ref.current) {
if (ref.current.contains(event.target)) return;
}
/*
* You do not need to remove the event handler here
* you create it when the component is mounted and
* remove it when it is unmounted via the "useEffect()" hook
*/
//document.removeEventListener('click', closeMenu);
setOpen(false);
}, []);
const toggleMenu = () => {
/*
* This is not necessary, you can do it in one line.
* Actually, you can entirely remove the function and
* call 'setOpen(!open)' directly from the button:
* <button onClick={() => {setOpen(!open)}}>
*/
//if (!open) {
// document.addEventListener('click', closeMenu);
// setOpen(true);
//} else {
// document.removeEventListener('click', closeMenu);
// setOpen(false);
//}
setOpen(!open);
};
useEffect(() => {
document.addEventListener('click', closeMenu);
return () => {
document.removeEventListener('click', closeMenu);
};
}, [closeMenu]);
return ( <
>
<
button onClick = {
toggleMenu
} > open menu < /button> {
open && < div ref = {
ref
} > menu < /div>} <
/>
);
};
export default DropDown;

Event Listener in react functional component

Essentially, I have this function component, but I'm struggling with adding the listener just to this component, since right now it's going to the entire window. I tried using useRef, but that wasn't working. Any ideas?
const AddReply =({item })=> {
const {replies, setReplies, artists, setArtists, messages, setMessages, songs, setSongs, posts, setPosts, likes, setLikes, user, setUser, accesstoken, setAccesToken, refreshtoken, setRefreshtoken} = React.useContext(InfoContext);
const refKey = useRef();
function eventhandler(e) {
if (e.code === 'Enter') {
handleSubmitReply();
console.log("Works");
}
}
function handleSubmitReply(){
console.log(document.getElementById("textareareply").value);
const likesref=dbLikes.push(0);
const likeskey=likesref.getKey();
console.log("Likes key is ", likeskey);
const ref = dbReplies.child(item['replies']).push({
'content':document.getElementById("textareareply").value,
'posterid': user.id,
'likes': likeskey,
"createdAt": {'.sv': 'timestamp'}
});
}
useEffect(() => {
// if I were to use useRef, then I tried using ref.current,
// but then I got that "TypeError: refKey.current.addEventListener
// is not a function".
window.addEventListener("keyup", eventhandler);
return () => {
window.removeEventListener("keyup", eventhandler);
};
}, []);
return(
<div>
<TextArea id="textareareply" ref={refKey} rows={1} placeholder='Reply to post' />
<Form.Button fluid positive onClick = {()=>handleSubmitReply()} style={{marginTop:"10px"}}>Reply</Form.Button>
</div>
)}
export default AddReply;```
Your useRef doesn't work probably because <TextArea> is a functional component so can't use ref. Instead, attach ref to the container:
<div ref={refKey}>
<TextArea id="textareareply" rows={1} placeholder='Reply to post' />
<Form.Button fluid positive onClick = {()=>handleSubmitReply()}
style={{marginTop:"10px"}}>Reply</Form.Button>
</div>
Then you can now attach event to it:
useEffect(() => {
const div = refKey.current;
div.addEventListener("keyup", eventhandler);
return () => {
div.removeEventListener("keyup", eventhandler);
};
}, []);
That should solve your problem, but if you still prefer the current way of listening on window without useRef, then just add logic to the event handler to act only on your component:
function eventhandler(e) {
if (e.target.getAttribute('id') === 'textareareply') {
if (e.code === 'Enter') {
console.log("Works");
}
}
}
When we use a functional component, we pass directly the function reference into DOM event attributes, such as:
function AddReply() {
function handleButtonClick(event) {
console.log(event)
}
return (
<button onClick={handleButtonClick}> Click Here </button>
);
}

React dropdown toggles only once

I am trying to build React Dropdown component using useRef hook and Typescript:
It opens correctly and closes if I click toggle button once or click outside of it, but it will closed when I want to open it again. Any ideas ? Is this I am loosing ref referance somehow ?
Here is usage:
https://codesandbox.io/s/react-typescript-obdgs
import React, { useState, useRef, useEffect } from 'react'
import styled from 'styled-components'
interface Props {}
const DropdownMenu: React.FC<Props> = ({ children }) => {
const [menuOpen, toggleMenu] = useState<boolean>(false)
const menuContent = useRef<HTMLDivElement>(null)
useEffect(() => {
// console.log(menuOpen)
}, [menuOpen])
const showMenu = (event: React.MouseEvent) => {
event.preventDefault()
toggleMenu(true)
document.addEventListener('click', closeMenu)
}
const closeMenu = (event: MouseEvent) => {
const el = event.target
if (menuContent.current) {
if (el instanceof Node && !menuContent.current.contains(el)) {
toggleMenu(false)
document.removeEventListener('click', closeMenu)
}
}
}
return (
<div>
<button
onClick={(event: React.MouseEvent) => {
showMenu(event)
}}
>
Open
</button>
{menuOpen ? <div ref={menuContent}>{children}</div> : null}
</div>
)
}
export default DropdownMenu
If you click the button twice, you will not be able to open it again. If you click outside the button to close, it will work as expected.
This is probably because your showMenu callback is executed even when the menu is already shown, which results in multiple closeMenu event listeners being attached, which in turn leads to weird behaviour.
The closeMenu event listener should be created inside an effect, not in the showMenu callback.
const showMenu = (event: React.MouseEvent) => {
event.preventDefault()
toggleMenu(true)
}
// closeMenu is the same
const closeMenu = (event: MouseEvent) => {
const el = event.target
if (menuContent.current) {
if (el instanceof Node && !menuContent.current.contains(el)) {
toggleMenu(false)
document.removeEventListener('click', closeMenu)
}
}
}
useEffect(() => {
if (!menuOpen) {
return
}
document.addEventListener('click', closeMenu)
return () => {
document.removeEventListener('click', closeMenu)
}
}, [menuOpen])
useEffect is really cool - the returned function where the event listener is removed will be called both when menuOpen is changed, and when the component is unmounted. In your previous code, if the component would be unmounted, the event listener would not be removed.
The problem comes from your onClick on the button. You are calling, showMenu every time you click on the button, so you are adding new event listener each time.
You don't want to call showMenu if the menu is already shown, so a fix can be :
<button onClick={(event: React.MouseEvent) => {
if (!menuOpen) showMenu(event);
}}>

Categories