I'm working on a form that initially only shows one input field and when it is focused, it shows other inputs and the submit button.
I also want to hide all those extra fields if the form loses focus while they are empty. And this is the part that I'm not being able to implement.
This is my code: I use a controlled form and a state to handle focus.
const FoldableForm = () => {
const [formState, setFormState] = useState(defaultFormState);
const [hasFocus, setFocus] = useState(false);
const handleOnBlur = () => {
if (!formState.message.trim() && !formState.other_input.trim()) {
setFocus(false);
}
};
return (
<form
onFocus={() => setFocus(true)}
onBlur={handleOnBlur}
>
<textarea
name="message"
onChange={(e) => setFormState({ ...formState, message: e.target.value })}
/>
{hasFocus && (
<>
<input
type="text" name="other_input"
onChange={(e) => setFormState({ ...formState, message: e.target.other_input })}
/>
<button type="button">Post comment</button>
</>
)}
</form>
);
}
Currently, if I type something in the text area, setFocus(false) is never invoked, so it works as intended.
Otherwise, if I leave it empty and click on the other input field, the handleOnBlur function is called, it sets focus to false, so the form is 'minimized'.
This is expected because the blur event (from the textarea) is triggered before the focus event (from the new input field). So I tried to use setTimeout to check, after a fraction of a second if the focus event had already occurred.
To do so, I used a second state (shouldShow) that is updated in a setTimeout inside the handleOnBlue function.
setTimeout(() => {
if(!hasFocus) {
setShouldShow(false); // this should cause the form to minimize
}
}, 100);
However, according to the react lifecycle, the value of hasFocus that is passed to the setTimeout function is at the invocation time, not at execution. So setTimeout here is useless.
I also tried to use references, but I couldn't make it work.
In your case i think that the usage of the shouldShow state is redundant and you can also avoid using a timeout which may lead to bugs.
You can take advantage of the FocusEvent.relatedTarget attribute and prevent hiding the extra fields when blur from an input and focus to another happens simultaneously.
The handleOnBlur function should look like this:
const handleOnBlur = (e) => {
if (e.relatedTarget && e.relatedTarget.name === "other_input") return;
if (!formState.message.trim() && !formState.other_input.trim()) {
setFocus(false);
}
};
You can find a working example in this code sandbox.
The problem with this approach is that if you have multiple fields appearing you need to check if any of those is focused like below:
["other_input", "another_input"].includes(e.relatedTarget.name)
This behavior is because of closures in JavaScript. The value of hasFocus is not the value of the variable at the moment your callback inside setTimeout is executed. It's the value when the onBlur callback is executed.
One solution would be to use functional updates.
Define a state which holds both hasFocus and shouldShow inside:
const [state, setState] = useState({ hasFocus: false, shouldShow: false });
When you try to access the previous state using functional updates, you get the most recent value:
setTimeout(() => {
setState((state) => {
if (!state.hasFocus) {
return { ...state, shouldShow: false };
}
return state;
});
}, 100);
codesandbox
Another solution would be to debounce a function which sets the hasFocus state to false, which imo is way better.
Related
export default function Form() {
const [user, setUser] = useState({
name: "",
numOfQs: 0
})
console.log(user)
function handleUserDataChange(event) {
setUser(prevUser => {
return {
...prevUser,
[event.target.name]: event.target.value
}
})
}
return (
<>
<input
type="text"
placeholder="username"
name="name"
value={user.name}
onChange={handleUserDataChange} />
<input
type="number"
name="numOfQs"
value={user.numOfQs}
onChange={handleUserDataChange} />
</>
)}
I was trying to build my form using react, and when I tried to use input[type: number] on the form field it was giving me this error, don't know why. I was reading through react docs about forms, and everything from the checkbox, radio buttons, textarea was all working fine. but when I used an input element of the type number, I got the following error.
*!Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property target on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist(). See fb.me/react-event-pooling for more information.
so, the problem only arises when an input of type "number" is introduced. when I remove it all of my other form elements work fine.
I'm still in the learning phase of react. please help me out.
This happened because the event that passed into the function is used as an asynchronous event.
To fix this, decompose the event object
function handleUserDataChange(event) {
const { name, value } = event.target;
setUser(prevUser => {
return {
...prevUser,
[name]: value
}
})
}
I have a React web app that is effectively a ton of Questions. These questions need to be validated/laid-out based on their own state values (ie: must be a number in a number field), as well as on the values of each other. A few examples of the more complex 'validation':
Questions A, B, and C might be required to have non-empty values before allowing a 'save' button.
Question B's allowable range of values might be dependent on the value of question A.
Question C might only show if question A is set to 'true'.
You can imagine many other interactions. The app has hundreds of questions - as such, I have their configuration in a JSON object like this:
{ id: 'version', required: true, label: 'Software Version', xs: 3 },
{
id: 'licenseType', label: 'License Type', xs: 2,
select: {
[DICTIONARY.FREEWARE]: DICTIONARY.FREEWARE,
[DICTIONARY.CENTER_LICENSE]: DICTIONARY.CENTER_LICENSE,
[DICTIONARY.ENTERPRISE_LICENSE]: DICTIONARY.ENTERPRISE_LICENSE
}
},
... etc.
I would then turn this object into actual questions using a map in the FormPage component, the parent of all the questions. Given the need to store these interaction in the closest common parent, I store all of the Question values in a formData state variable object and the FormPage looks like so:
function FormPage(props) {
const [formData, setFormData] = useState(BLANK_REQUEST.asSubmitted);
const handleValueChange = (evt, id) => {
setFormData({ ...formData, [id]: evt.target.value})
}
return <div>
{QUESTIONS_CONFIG.map(qConfig => <Question qConfig={qConfig} value={formData[qConfig.id]} handleValueChange={handleValueChange}/>)}
// other stuff too
</div>
}
The Question component is basically just a glorified material UI textField that has it's value set to props.value and it's onChange set to props.handleValueChange. The rest of the qConfig object and Question component is about layout and irrelevant to the question.
The problem with this approach was that every keypress results in the formData object changing... which results in a re-render of the FormPage component... which then results in a complete re-render/rebuild of all my hundreds of Question components. It technically works, but results performance so slow you could watch your characters show up as you type.
To attempt solve this, I modified Question to hold it's own value in it's own state and we no longer pass formData to it... the Question component looking something like this:
function Question(props) {
const { qConfig, valueChangedListener, defaultValue } = props;
const [value, setValue] = useState(props);
useEffect(() => {
if (qConfig.value && typeof defaultValue !== 'undefined') {
setValue(qConfig.value);
}
}, [qConfig.value])
const handleValueChange = (evt, id) => {
setValue(evt.target.value);
valueChangedListener(evt.target.value, id)
}
return <div style={{ maxWidth: '100%' }}>
<TextField
// various other params unrelated...
value={value ? value : ''}
onChange={(evt) => handleValueChange(evt, qConfig.id)}
>
// code to handle 'select' questions.
</TextField>
</div>
}
Notably, now, when it's value changes, it stores it's own value only lets FormPage know it's value was updated so that FormPage can do some multi-question validation.
To finish this off, on the FormPage I added a callback function:
const processValueChange = (value, id) => {
setFormData({ ...formData, [id]: value })
};
and then kept my useEffect that does cross-question validation based on the formData:
useEffect(() => { // validation is actually bigger than this, but this is a good example
let missingArr = requiredFields.filter(requiredID => !formData[requiredID]);
setDisabledReason(missingArr.length ? "Required fields (" + missingArr.join(", ") + ") must be filled out" : '');
}, [formData, requiredFields]);
the return from FormPage had a minor change to this:
return <div>
{questionConfiguration.map(qConfig =>
<Question
qConfig={qConfig}
valueChangedListener={processValueChange}
/>
</ div>
)
}
Now, my problem is -- ALL of the questions still re-render on every keypress...
I thought that perhaps the function I was passing to the Question component was being re-generated so I tried wrapping processValueChange in a useCallback:
const processValueChange = React.useCallback((value, id) => {
setFormData({ ...formData, [id]: value })
}
},[]);
but that didn't help.
My guess is that even though formData (a state object on the FormPage) is not used in the return... its modification is still triggering a full re-render every time.
But I need to store the value of the children so I can do some stuff with those values.
... but if I store the value of the children in the parent state, it re-renders everything and is unacceptbaly slow.
I do not know how to solve this? Help?
How would a functional component store all the values of its children (for validation, layout, etc)... without triggering a re-render on every modification of said data? (I'd only want a re-render if the validation/layout function found something that needed changing)
EDIT:
Minimal sandbox: https://codesandbox.io/s/inspiring-ritchie-b0yki
I have a console.log in the Question component so we can see when they render.
This is my React Hook:
function Student(props){
const [open, setOpen] = useState(false);
const [tags, setTags] = useState([]);
useEffect(()=>{
let input = document.getElementById(tagBar);
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById(tagButton).click();
}
});
},[tags])
const handleClick = () => {
setOpen(!open);
};
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
tagList.push(input.value);
console.log("tag");
console.log(tags);
console.log("taglist");
console.log(tagList);
setTags(tagList);
}
const tagDisplay = tags.map(t => {
return <p>{t}</p>;
})
return(
<div className="tags">
<div>
{tagDisplay}
</div>
<input type='text' id={tagBar} className="tagBar" placeholder="Add a Tag"/>
<button type="submit" id={tagButton} className="hiddenButton" onClick={addTag}></button>
<div>
);
What I am looking to do is be able to add a tag to these student elements (i have multiple but each are independent of each other) and for the added tag to show up in the tag section of my display. I also need this action to be triggerable by hitting enter on the input field.
For reasons I am not sure of, I have to put the enter binding inside useEffect (probably because the input element has not yet been rendered).
Right now when I hit enter with text in the input field, it properly updates the tags/tagList variable, seen through the console.logs however, even though I set tags to be the re-rendering condition in useEffect (and the fact that it is also 1 of my states), my page is not updating with the added tags
You are correct, the element doesn't exist on first render, which is why useEffect can be handy. As to why its not re-rendering, you are passing in tags as a dependency to check for re-render. The problem is, tags is an array, which means it compares the memory reference not the contents.
var myRay = [];
var anotherRay = myRay;
var isSame = myRay === anotherRay; // TRUE
myRay.push('new value');
var isStillSame = myRay === anotherRay; // TRUE
// setTags(sameTagListWithNewElementPushed)
// React says, no change detected, same memory reference, skip
Since your add tag method is pushing new elements into the same array reference, useEffect thinks its the same array and is not re-triggers. On top of that, React will only re-render when its props change, state changes, or a forced re-render is requested. In your case, you aren't changing state. Try this:
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
// Create a new array reference with the same contents
// plus the new input value added at the end
setTags([...tagList, input.value]);
}
If you don't want to use useEffect I believe you can also use useRef to get access to a node when its created. Or you can put the callback directly on the node itself with onKeyDown or onKeyPress
I can find few mistake in your code. First, you attaching event listeners by yourself which is not preferred in react. From the other side if you really need to add listener to DOM inside useEffect you should also clean after you, without that, another's listeners will be added when component re-rendered.
useEffect( () => {
const handleOnKeyDown = ( e ) => { /* code */ }
const element = document.getElementById("example")
element.addEventListener( "keydown", handleOnKeyDown )
return () => element.removeEventListener( "keydown", handleOnKeyDown ) // cleaning after effect
}, [tags])
Better way of handling events with React is by use Synthetic events and components props.
const handleOnKeyDown = event => {
/* code */
}
return (
<input onKeyDown={ handleOnKeyDown } />
)
Second thing is that each React component should have unique key. Without it, React may have trouble rendering the child list correctly and rendering all of them, which can have a bad performance impact with large lists or list items with many children. Be default this key isn't set when you use map so you should take care about this by yourself.
tags.map( (tag, index) => {
return <p key={index}>{tag}</p>;
})
Third, when you trying to add tag you again querying DOM without using react syntax. Also you updating your current state basing on previous version which can causing problems because setState is asynchronous function and sometimes can not update state immediately.
const addTag = newTag => {
setState( prevState => [ ...prevState, ...newTage ] ) // when you want to update state with previous version you should pass callback which always get correct version of state as parameter
}
I hope this review can help you with understanding React.
function Student(props) {
const [tags, setTags] = useState([]);
const [inputValue, setInputValue] = useState("");
const handleOnKeyDown = (e) => {
if (e.keyCode === 13) {
e.preventDefault();
addTag();
}
};
function addTag() {
setTags((prev) => [...prev, inputValue]);
setInputValue("");
}
return (
<div className="tags">
<div>
{tags.map((tag, index) => (
<p key={index}>{tag}</p>
))}
</div>
<input
type="text"
onKeyDown={handleOnKeyDown}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a Tag"
/>
<button type="submit" onClick={addTag}>
ADD
</button>
</div>
);
}
I have an input number value which I'm trying to send to my clickHandler but I've done something wrong ...
On click I want to send the value "this.state.NumberHolder" to the handler
<input value={this.state.NumberHolder} onClick={this.clickHandler} type="number" />
Doing a console.log I can see that my clickHandler is being called but I'm not getting the updated state value
clickHandler = (target) => {
console.log("targetHere", target);
this.setState({
NumberHolder: target.value
});
};
Actually, what you receive by default property is the context of the event.
So, to handle correctly the value of the input tag, you need to do this:
clickHandler = (event) => {
console.log("targetHere", event.target);
this.setState({
NumberHolder: event.target.value
});
};
And there is a big issue with your JSX, onClick is executed when the input is clicked, not changed. So, you will never receive the new input value. Use on change:
<input value={this.state.NumberHolder} onChange={this.clickHandler} type="number" />
And this should work perfectly. Check this fiddle to see it working.
I believe it should be like this:
// destructure target from the event object by wrapping it in braces
clickHandler = ({target}) => {
console.log("targetHere", target);
this.setState({
NumberHolder: target.value
});
};
But there is a bigger issue with your code. Since the value of your input will always be this.state.NumberHolder, you are simply setting the same value over and over again.
If you have a particular value you want to send on the click event, you can turn your click event into a curried function like this:
// pass in number as first argument to function that returns another anonymous function
clickHandler = (NumberHolder) => () =>{
this.setState({ NumberHolder });
};
And then on the element with the click event, pass the onClick like this:
<input onClick={this.clickHandler(3)} />
That will pass the argument in scope of the function, and allow you to access it.
Considering your comments, i believe you want to add the event on change of the input and log the value entered like this:
<input value={this.state.NumberHolder} onChange={this.clickHandler} type="number" />
clickHandler = (target) => {
console.log("target Value", target.value);
this.setState({
NumberHolder: target.value
});
};
When I update the value in my input field, the cursor moves to the end of the field, but I want it to stay where it is. What could be causing this issue?
<Input
type="text"
placeholder="test
name="test"
onChange={getOnChange(index)}
value={testVal}/>
where Input is a component for the text input field, and getOnChange is:
const getOnChange = (index) =>
(event) => props.onChangeTest(event, index);
This is then carried over to the parent component, where I dispatch to update the state via Redux. I can see that the state is being updated fine, but the problem is the cursor is not staying in position, and is always moving to the end of the text
If the cursor jumps to the end of the field it usually means that your component is remounting. It can happen because of key property change on each update of the value somewhere in your parent or changes in your components tree. It's hard to tell without seeing more code. Prevent remounting and the cursor should stop jumping.
Use this effect to track mounting/unmounting
useEffect(() => {
console.log('mounted');
return () => {
console.log('unmounted')
}
}, []);
I would suggest using hooks to solve this
const Component = ({ onChange }) => {
const [text, setText] = useState("");
const isInitialRun = useRef(false);
useEffect(() => {
if (isInitialRun.current) {
onChange(text);
} else {
isInitialRun.current = true;
}
}, [text]);
// or if you want to have a delay
useEffect(() => {
if (isInitialRun.current) {
const timeoutId = setTimeout(() => onChange(text), 500);
return () => clearTimeout(timeoutId);
} else {
isInitialRun.current = true;
}
}, [text])
return (
<Input
type="text"
placeholder="test
name="test"
onChange={setText}
value={text}/>
);
}
To prevent initial call, when nothing changed isInitialRun used
This is the downside of the controlled component design pattern. I've been facing this problem for a long time and just lived with it. But there's an idea that I wanted to try in my spare time but end up never trying it (yet). Perhaps continuing with my idea could help you come up with the solution you need?
<Input
type="text"
placeholder="test
name="test"
onChange={getOnChange(index)}
value={testVal}
/>
// From props.onChangeTest
const onChangeTest = (event, index) => {
// TODO: Memorize the position of the cursor
this.setState({ testVal: event.target.value })
// Because setState is asynchronous
setTimeout(() => {
// TODO:
// Programmatically move cursor back to the saved position
// BUT it must increase/decrease based on number of characters added/removed
// At the same time considering if the characters were removed before or after the position
// Theoretically do-able, but it's very mind-blowing
// to come up with a solution that can actually 'nail it'
}, 0)
}
★ If this is taking too much time and you just want to get work done and ship your app, you might wanna consider using the uncontrolled component design pattern instead.
I was facing same issue, it was due to 2 sequential setState statements. changing to single setState resolved the issue. Might be helpful for someone.
Code before fix:
const onChange = (val) => {
// Some processing here
this.setState({firstName: val}, () => {
this.updateParentNode(val)
})
}
const updateParentNode = (val) => {
this.setState({selectedPerson: {firstName: val}})
}
Code After Fix
const onChange = (val) => {
// Some processing here
this.updateParentNode(val)
}
const updateParentNode = (val) => {
this.setState({selectedPerson: {firstName: val}, firstName: val})
}
You have two options.
make it an uncontrolled input (you can not change the input value later)
make it a properly controlled input
There is code missing here, so I can't say what the problem is.
setState is not the issue: https://reactjs.org/docs/forms.html#controlled-components
If you use setState in the callback React should preserve the cursor position.
Can you give a more complete example?
Is testVal a property that is manipulated from outside the component?
This totally worked for me (the other solutions did not):
const handleChange = (e, path, data) => {
let value = _.isObject(data) ? data.value : data;
let clonedState = { ...originalState };
// save position of cursor
const savedPos = e.target.selectionStart;
_.set(clonedState, path, value); // setter from lodash/underscore
// this wil move cursor to the end
setState({ ...clonedState }); // some use state setter
setTimeout(() => {
// restore cursor position
e.target.setSelectionRange(savedPos, savedPos);
}, 0)
};
Have this on my template (using semantic-ui):
<Input
type="text"
readOnly={false}
onChange={(e, data) => {
handleChange(e, "field", data);
}}
value={state.field}>
</Input>
For me, I was having a <ComponentBasedOnType> and that was the issue, I changed my logic and render my components with a condition && in the parent component.
The cursor on an input will be pushed to the end when you dynamically update the input value through code, which it seems like you are doing because I can see value={testVal} :)
This is a common issue on fields that use input masking!