I'm new to React and trying to understand why my state value is blank. In my component, I have a state for the HTML input element. When I put a console log on the function "searchChange" handler it is properly receiving my keyboard entries. But when I click on the enter key the value of "searchState" is blank. So I'm wondering what is wrong with my code?
export default (props: any, ref: any) => {
const SearchNavigation = forwardRef((props: any, ref: any) => {
const [searchState, setSearchState] = React.useState('');
const searchChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
console.log(`Input ${evt.target.value}`);
setSearchState(evt.target.value);
};
function enterHandler(evt: any): any {
if ((evt.which && evt.which === 13) || (evt.keyCode && evt.keyCode === 13)) {
console.log(`Enter value ${searchState}`);
return false;
}
}
useEffect(() => {
document.body.addEventListener('keypress', enterHandler);
}, []);
return (
<section id={props.id} className="is-shadow-d2">
<input type="text" onChange={searchChange} />
</section>
);
}
If I add the following hook, it is logging my value of "searchState" properly as I type each letter, but I want to use the value when the users clicks on the enter key.
useEffect(() => {
console.log(`State Changed: ${searchState}`);
}, [searchState]);
Update
So I modified my code to handle a keypress on the input and seems to work.
const searchKeyPress = (evt: React.KeyboardEvent) => {
if (evt.key === 'Enter') {
console.log(`Enter value ${searchState}`);
}
};
....
<input type="text" onChange={searchChange} onKeyPress={searchKeyPress} />
And now its showing the values. But what I'm wondering about is if I want to have a global enter key to do something like save the data but the states values are blank, what is the best way to handle that?
useEffect(() => {
document.body.addEventListener('keypress', enterHandler);
}, []);
The empty dependency array says that this effect should be run once, and then never again. At the time that the effect runs, searchState is an empty string, and so enterHandler closes over that empty string. Since the functions never get updated, it will still see an empty string when the keypress occurs.
Instead, you can change it to update when the searchState changes like this:
useEffect(() => {
function enterHandler(evt: any): any {
if ((evt.which && evt.which === 13) || (evt.keyCode && evt.keyCode === 13)) {
console.log(`Enter value ${searchState}`);
return false;
}
}
document.body.addEventListener('keypress', eventHandler);
return () => {
document.body.removeEventListener('keypress', eventHandler);
}
}, [searchState]);
The issue is that states are not accessible in the context of event listeners.
You can find an answer to this problem here: Wrong React hooks behaviour with event listener
Related
So I'm in a situation where I can't use useReff because I have a big state where I need to edit the value directly on that state, but I also don't want the state to change and render that specific component because is to heavy and I have lag rendering the component, more exactly I'm using quill-react, and this component is big so It has so lag in input with many renders.
So I have done a function that when the user stops typing after 2 seconds a function is called, and in case it doesn't stop the function set the states to true always so the function doesn't call when is checked, but is not working for some reason, I'm kinda new on this stuff.
Here is what I have done until now, and don't forget I'm iterating a list of inputs and then managing them with setState on another component, using the method: Work.Description:
const [call, setCall] = useState(false);
const [editors, setEditors] = useState({});
const [currentIndex, setCurrentIndex] = useState("");
const handleDescription = (e: any, index: number) => {
setCurrentIndex(index);
setEditors( { ...editors, [index]: e } )
setTimeout(() => setCall(true), 1000)
}
useEffect(() => {
setInterval(() => {
if (call === true) {
Work.Description(props, currentIndex, editors[currentIndex])
setCurrentIndex("")
setCall(false)
}
}, 1000)
}, [])
return (
<Container>
<Items>
{
props.resume.employmentHistory.positions.map(
(position: any, index: number) =>
<Editor
label="Description"
value={position.description && position.description || ''}
onChange={(e) => handleDescription(e, index)}
/>
)
}
</Items>
</Container >
)
As I mentioned, I want something like this not that fancy with useReff or other methods, let's see what we can do.
I think debounce might be what you're looking for, is that correct? Do something when user stops doing something for n milliseconds.
If yes then you probably don't need timeout and interval. I think you forgot to clear those timeouts/intervals by the way!
So you can do it like this (don't forget to install #types/lodash.debounce if you're using TypeScript):
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
// Rename this to fit your needs.
const callWorkDescription = useCallback(
debounce((props, currentIndex, editor) => {
Work.Description(props, currentIndex, editor)
setCurrentIndex("")
}, 2000),
[]
)
const handleDescription = (e: any, index: number) => {
setCurrentIndex(index);
setEditors((prev) => {
const nextEditors = { ...editors, [index]: e }
callWorkDescription(props, index, nextEditors[index])
return nextEditors
})
}
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 am using primereact's Autocomplete component. The challenge is that I don't want to set the options array to the state when the component loads; but instead I fire an api call when the user has typed in the first 3 letters, and then set the response as the options array (This is because otherwise the array can be large, and I dont want to bloat the state memory).
const OriginAutocomplete = () => {
const [origins, setOrigins] = useState([]);
const [selectedOrigin, setSelectedOrigin] = useState(null);
const [filteredOrigins, setFilteredOrigins] = useState([]);
useEffect(() => {
if (!selectedOrigin || selectedOrigin.length < 3) {
setOrigins([]);
}
if (selectedOrigin && selectedOrigin.length === 3) {
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
});
}
}, [selectedOrigin, setOrigins]);
const handleSelect = (e) => {
//update store
}
const searchOrigin = (e) => {
//filter logic based on e.query
}
return (
<>
<AutoComplete
value={selectedOrigin}
suggestions={ filteredOrigins }
completeMethod={searchOrigin}
field='code'
onChange={(e) => { setSelectedOrigin(e.value) }}
onSelect={(e) => { handleSelect(e) }}
className={'form-control'}
placeholder={'Origin'}
/>
</>
)
}
Now the problem is that the call is triggered when I type in 3 letters, but the options is listed only when I type in the 4th letter.
That would have been okay, infact I tried changing the code to fire the call when I type 2 letters; but then this works as expected only when I key in the 3rd letter after the api call has completed, ie., I type 2 letters, wait for the call to complete and then key in the 3rd letter.
How do I make the options to be displayed when the options array has changed?
I tried setting the filteredOrigins on callback
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
setFilteredOrigins([...origins])
});
But it apparently doesn't seem to work.
Figured it out. Posting the answer in case someone ponders upon the same issue.
I moved the code inside useEffect into the searchOrigin function.
SO the searchOrigin functions goes like below:
const searchOrigin = (e) => {
const selectedOrigin = e.query;
if (!selectedOrigin || selectedOrigin.length === 2) {
setOrigins([]);
setFilteredOrigins([]);
}
if (selectedOrigin && selectedOrigin.length === 3) {
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
setFilteredOrigins(origins);
});
}
if (selectedOrigin && selectedOrigin.length > 3) {
const filteredOrigins = (origins && origins.length) ? origins.filter((origin) => {
return origin.code
.toLowerCase()
.startsWith(e.query.toLowerCase()) ||
origin.name
.toLowerCase()
.startsWith(e.query.toLowerCase()) ||
origin.city
.toLowerCase()
.startsWith(e.query.toLowerCase())
}) : [];
setFilteredOrigins(filteredOrigins);
}
}
I am using hook api for managing state, the problem is that state is sometimes empty in handler fucntion.
I am using component for manage contenteditable (using from npm) lib. You can write to component and on enter you can send event to parent.
See my example:
import React, { useState } from "react"
import css from './text-area.scss'
import ContentEditable from 'react-contenteditable'
import { safeSanitaze } from './text-area-utils'
type Props = {
onSubmit: (value: string) => void,
}
const TextArea = ({ onSubmit }: Props) => {
const [isFocused, setFocused] = useState(false);
const [value, setValue] = useState('')
const handleChange = (event: React.FormEvent<HTMLDivElement>) => {
const newValue = event?.currentTarget?.textContent || '';
setValue(safeSanitaze(newValue))
}
const handleKeyPress = (event: React.KeyboardEvent<HTMLDivElement>) => {
// enter
const code = event.which || event.keyCode
if (code === 13) {
console.log(value) // THERE IS A PROBLEM, VALUE IS SOMETIMES EMPTY, BUT I AM SURE THAT TEXT IS THERE!!!
onSubmit(safeSanitaze(event.currentTarget.innerHTML))
setValue('')
}
}
const showPlaceHolder = !isFocused && value.length === 0
const cls = [css.textArea]
if (!isFocused) cls.push(css.notFocused)
console.log(value) // value is not empty
return (
<ContentEditable
html={showPlaceHolder ? 'Join the discussion…' : value}
onChange={handleChange}
className={cls.join(' ')}
onClick={() => setFocused(true)}
onBlur={() => setFocused(false)}
onKeyPress={handleKeyPress}
/>
)
}
export default React.memo(TextArea)
Main problem is that inside handleKeyPress (after enter keypress) is value (from state) empty string, why? - in block console.log(value) // THERE IS A PROBLEM, VALUE IS SOMETIMES EMPTY, BUT I AM SURE THAT TEXT IS THERE!!! I don't understand what is wrong??
The value is empty, because onChange doesn't actually change it, which means
const newValue = event?.currentTarget?.textContent || '';
this line doesn't do what it's supposed to. I think you should read the target prop in react's synthetic events instead of currentTarget. So, try this instead
const newValue = event.target?.value || '';
Hope this helps.
I have this application that has a deprecated lifecycle method:
componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
Currently, I have used the UNSAFE_ flag:
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
I have left it like this because when I attempted to refactor it to:
componentDidUpdate(prevProps, prevState) {
if (this.state.displayErrors) {
this._validate(prevProps, prevState);
}
}
It created another bug that gave me this error:
Invariant Violation: Maximum update depth exceeded. This can happen
when a component repeatedly calls setState inside componentWillUpdate
or componentDidUpdate. React limits the number of nested updates to
prevent infinite loops.
It starts to happen when a user clicks on the PAY NOW button that kicks off the _handlePayButtonPress which also checks for validation of credit card information like so:
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
_validate = props => {
const { cardExpireDate, cardNumber, csv, nameOnCard } = props;
const validationErrors = {
date: cardExpireDate.trim() ? "" : "Is Required",
cardNumber: cardNumber.trim() ? "" : "Is Required",
csv: csv.trim() ? "" : "Is Required",
name: nameOnCard.trim() ? "" : "Is Required"
};
if (validationErrors.csv === "" && csv.trim().length < 3) {
validationErrors.csv = "Must be 3 or 4 digits";
}
const fullErrors = {
...validationErrors,
...this.props.validationErrors
};
const isValid = Object.keys(fullErrors).reduce((acc, curr) => {
if (fullErrors[curr]) {
return false;
}
return acc;
}, true);
if (isValid) {
this.setState({ validationErrors: {} });
//register
} else {
this.setState({ validationErrors, displayErrors: true });
}
return isValid;
};
_handlePayButtonPress = () => {
const isValid = this._validate(this.props);
if (isValid) {
console.log("Good to go!");
}
if (isValid) {
this.setState({ processingPayment: true });
this.props
.submitEventRegistration()
.then(() => {
this.setState({ processingPayment: false });
//eslint-disable-next-line
this.props.navigation.navigate("PaymentConfirmation");
})
.catch(({ title, message }) => {
Alert.alert(
title,
message,
[
{
text: "OK",
onPress: () => {
this.setState({ processingPayment: false });
}
}
],
{
cancelable: false
}
);
});
} else {
alert("Please correct the errors before continuing.");
}
};
Unfortunately, I do not have enough experience with Hooks and I have failed at refactoring that deprecated lifecycle method to one that would not create trouble like it was doing with the above error. Any suggestions at a better CDU or any other ideas?
You need another check so you don't get in an infinite loop (every time you call setState you will rerender -> component did update -> update again ...)
You could do something like this:
componentDidUpdate(prevProps, prevState) {
if (this.state.displayErrors && prevProps !== this.props) {
this._validate(prevProps, prevState);
}
}
Also I think that you need to call your validate with new props and state:
this._validate(this.props, this.state);
Hope this helps.
componentDidUpdate shouldn't replace componentWillRecieveProps for this reason. The replacement React gave us was getDerivedStateFromProps which you can read about here https://medium.com/#baphemot/understanding-react-react-16-3-component-life-cycle-23129bc7a705. However, getDerivedStateFromProps is a static function so you'll have to replace all the setState lines in _validate and return an object instead.
This is how you work with prevState and hooks.
Working sample Codesandbox.io
import React, { useState, useEffect } from "react";
import "./styles.css";
const ZeroToTen = ({ value }) => {
const [myValue, setMyValue] = useState(0);
const [isValid, setIsValid] = useState(true);
const validate = value => {
var result = value >= 0 && value <= 10;
setIsValid(result);
return result;
};
useEffect(() => {
setMyValue(prevState => (validate(value) ? value : prevState));
}, [value]);
return (
<>
<span>{myValue}</span>
<p>
{isValid
? `${value} Is Valid`
: `${value} is Invalid, last good value is ${myValue}`}
</p>
</>
);
};
export default function App() {
const [value, setValue] = useState(0);
return (
<div className="App">
<button value={value} onClick={e => setValue(prevState => prevState - 1)}>
Decrement
</button>
<button value={value} onClick={e => setValue(prevState => prevState + 1)}>
Increment
</button>
<p>Current Value: {value}</p>
<ZeroToTen value={value} />
</div>
);
}
We have two components, one to increase/decrease a number and the other one to hold a number between 0 and 10.
The first component is using prevState to increment the value like this:
onClick={e => setValue(prevState => prevState - 1)}
It can increment/decrement as much as you want.
The second component is receiving its input from the first component, but it will validate the value every time it is updated and will allow values between 0 and 10.
useEffect(() => {
setMyValue(prevState => (validate(value) ? value : prevState));
}, [value]);
In this case I'm using two hooks to trigger the validation every time 'value' is updated.
If you are not familiar with hooks yet, this may be confusing, but the main idea is that with hooks you need to focus on a single property/state to validate changes.