Input value resets when input is cleared - javascript

I have an input in a react component to store a name:
<input key="marker-name" id="marker-name" name="marker-name" onChange={handleRename} type="text" value={name} />
I have written the following handler for it:
const handleRename = ({ target }) => {
setPerception({
...perception,
name: target.value
})
}
However, it's not working as expected, if a user tries to delete the existing name then as soon as the last character in the input is deleted (i.e. the input is empty) the previous value just pops back in.
Here is the full code of the component:
import React, { useState, useEffect } from 'react'
// import custom styles for child component
import './styles.scss'
const MarkerName = ({ store, onStoreUpdate, callbackFunction }) => {
const [clicked, setClicked] = useState(false)
const [perception, setPerception] = useState(null)
const [currentMarkerName] = useState(store.currentMarkerName)
const [currentMarkerForce] = useState(store.currentMarkerForce)
const [currentForce] = useState(store.currentForce)
// A copy of the store to capture the updates
const newStore = store
// Only populate the perception state if it's store value exists
useEffect(() => {
store.perception && setPerception(store.perception)
}, [])
// Only show the form to non-umpire players who cannot see the correct name
const clickHander = () =>
currentForce !== 'umpire' &&
currentForce !== currentMarkerForce &&
setClicked(true)
const handleRename = ({ target }) => {
setPerception({
...perception,
name: target.value
})
newStore.perception.name = target.value
onStoreUpdate(newStore)
}
const handleSubmit = e => {
e && e.preventDefault()
callbackFunction(newStore)
}
const handleRevert = e => {
e.preventDefault()
setPerception({
...perception,
name: null
})
newStore.perception.name = null
onStoreUpdate(newStore)
handleSubmit()
}
const name = perception && perception.name ? perception.name : currentMarkerName
return (
<>
<h2 key="header" onClick={clickHander}>{name}</h2>
{
clicked &&
<div className="input-container marker-name">
<label>
Update asset name
<input key="marker-name" id="marker-name" name="marker-name" onChange={handleRename} type="text" value={name} />
</label>
<button type="submit" onClick={handleSubmit}>Rename</button>
<button onClick={handleRevert}>Revert</button>
</div>
}
</>
)
}
export default MarkerName

As far as I can tell this line is the problem:
const name = perception && perception.name ? perception.name : currentMarkerName;
You are re-rendering on every character change (onChange={handleRename}). As soon as all characters are deleted perception && perception.name is evaluated to true && false (empty strings are falsy) which is false. So the component is rendered with const name = currentMarkerName. As currentMarkerName hasn't changed yet, it is re-rendered with the old name.
Use this instead:
const name = perception && typeof perception.name !== 'undefined' ? perception.name : currentMarkerName;

In React forms are controlled components, You were almost getting it but at that point where you checked for the value of perception before assigning to the inputValue...that does not seem right.
Could you try to make these changes and let us see the effect:
1. For the state value of perception, make the initial value any empty string:
const [perception, setPerception] = useState(null)
On the forminput use
The handleRename function could just be declared as
const handleRename = (e) => {e.target.name: e.target.value}

Related

IonInput not allowing to conditionally prevent the onChange event

On this StackBlitz project: https://stackblitz.com/edit/node-hxolmq?file=src%2Fmain.tsx
I have the following custom control...
/src/controls/IonInputMagic2.js
import { IonInput } from "#ionic/react";
import { useEffect, useState } from "react";
const IonInputMagic2 = props => {
const { value, onChange, validityFunc, ...others } = props
var isValidValue = validityFunc(value);
const initialValue = (typeof value !== 'undefined' && isValidValue) ? value : '';
const [ currentValue, setCurrentValue ] = useState(initialValue);
useEffect(() => {
setCurrentValue(initialValue);
}, [initialValue]);
const handleChange = (e) => {
var value = e.target.value;
if (!validityFunc(value)) {
e.preventDefault();
return false;
}
setCurrentValue(value);
if (onChange) {
onChange(e);
}
};
return (
<IonInput value={currentValue} onChange={handleChange} {...others} />
);
}
export default IonInputMagic2;
where you can see I use the Ionic control: IonInput.
My problem is: I have a validityFunc(...) that decides if what the user enters is acceptable or not. As per that function, only numeric and even digits are allowed. However, the user can enter whatever character with no restrictions.
I have a similar control: IonInputMagic1 which is very similar, but it uses the HTML built-in element: <input /> instead of the Ionic control: <IonInput />. On that control the user can only enter what is expected: only numeric and even digits.
Here is the difference between those 2 controls (left: works | right: doesn't work)
Here is how I use both controls:
What I need is: To make IonInputMagic2 (which uses: IonInput) work as: IonInputMagic1 where the user can only enter numeric and even digits. This is because the IonInput uses all the styling and scripting of Ionic and I don't want to break all that by using: <input />.
Note: I have detected through the DOM that the IonInput is a wrapper of: <input />.
Any idea on how to achieve that?
If possible, please fork the StackBlitz above and post the link here.
Thanks!
This change did the trick:
Here the full code for the component:
import { IonInput } from "#ionic/react";
import { useEffect, useState } from "react";
const IonInputMagic2 = props => {
const { value, onChange, validityFunc, ...others } = props
var isValidValue = validityFunc(value);
const initialValue = (typeof value !== 'undefined' && isValidValue) ? value : '';
const [currentValue, setCurrentValue] = useState(initialValue);
useEffect(() => {
setCurrentValue(initialValue);
}, [initialValue]);
const handleChange = (e) => {
var value = e.target.value;
if (!validityFunc(value)) {
e.preventDefault();
e.target.value = currentValue;
return false;
}
setCurrentValue(value);
if (onChange) {
onChange(e);
}
};
return (
<IonInput value={currentValue} onIonInput={handleChange} {...others} />
);
}
export default IonInputMagic2;

How to clear n number of input fields dynamically on a single button click in react

const { useState, useEffect } = React;
const Thingy = ({ ...props }) => {
const [tenure, setTenure] = useState(null);
// state to hold tenure-dates (array of varying size)
const [tnDates, setTnDates] = useState(null);
const handleTenureChange = ev => setTenure(ev.target.value);
useEffect(() => setTnDates(
(tenure && tenure > 0)
? ([...Array(+tenure).keys()].map(
id => ({ id, tenureDate: '' })
)) : null
), [tenure]);
const handleDateChange = ev => {
const idx = ev.target.id;
const val = ev.target.value;
setTnDates(prev => {
const nd = [...prev];
nd[idx].tenureDate = val;
return nd;
});
};
above is the snippet for rendering tenure number of tenuredata where tenure is input
from user.
i want to clear all the tenure data input fields on a single button click. please help on this.
Since the input fields are presumably the states of both
const [tenure, setTenure] = useState(null);
const [tnDates, setTnDates] = useState(null);
To 'clear' the above state you need to reset it to it's original falsy value.
Since both the states you want to set are initially set to null you simply need to set their state to null again.
// button that clears the form fields
<button onClick={() => {
setTenure(null);
setTnDates(null);
}
Make sure you are using controlled components.
const handleClearInputs = () => {
setTenure(null)
setTnDates(null);
}
return(
<input value={tenure} />
<input value={tnDate} />
<button onClick={handleClearInputs}>Clear</button>
)

React state saving first value empty

I have the following setup in my react app:
const [modalReasonInput, setModalReasonInput] = useState("")
const validateReason = () => {
console.log(modalReasonInput)
if (modalReasonInput === "" && record.judgement_result === "fail") {
setError("Field is required")
return false
}
setError("")
return true
}
const handleChange = (value) => {
setModalReasonInput(value)
validateReason()
}
<TextField
type="textarea"
maxLength={1000}
showCounting
rows={6}
value={modalReasonInput}
onBlur={() => validateReason()}
onChange={(e) => handleChange(e.target.value)}
/>
With the above code when I enter first character in the input field a blank/white space is set as value to state modalReasonInput.
The value is set only if I type another character, but value is set to first character I had typed.
Due to this my validation is failing while typing first character.
How can I update the state value as I type.
It is getting updated as you type but when you try to console.log the state right after you call setModalReasonInput you can't see it right away because state updates in React is asynchronous. You can check the state with useEffect as the state changes:
useEffect(() => {
console.log(modalReasonInput)
}, [modalReasonInput])
The problem is your handleChange function. If you set the state and perform a validation shortly after it is not guaranteed that the state is available in the next line.
const handleChange = (value) => {
setModalReasonInput(value)
validateReason() // value not guarenteed to be in modalReasonInput state
}
You can either separate state update and validation or directly pass in your value to your validation function validateReason():
const validateReason = (value) => {
console.log(value)
if (value === "" && record.judgement_result === "fail") {
setError("Field is required")
return false
}
setError("")
return true
};
const handleChange = (value) => {
setModalReasonInput(value)
validateReason(value)
}
const [modalReasonInput, setModalReasonInput] = useState("")
const reasonRef = useRef("")
const validateReason = () => {
console.log(easonRef.current)
if (reasonRef.current === "" && record.judgement_result === "fail") {
setError("Field is required")
return false
}
setError("")
return true
}
const handleChange = (value) => {
setModalReasonInput(value)
easonRef.current = value
validateReason()
}
Thanks everyone I ended up using useRef hook for this issue. I didn't use #sm3sher approach because validateReason is being called by other methods also which do not get event value.
I didn't use #Enes method useEffect because judgement_result is initially set to null.

How to implement two methodes from validation on a form in the react?

I have a project that is only allowed me to change this file (file below). Other files are complete, for this reason, I do not place these in here (also do not lengthen the code). My question is only about the implementation of two methods from validation in this file.
command:
An error is not displayed during the first focus on input until taken focus from it. If there is an error, it displays. and next times, during of the changing if encounter with an error, the same time displays.
I implemented the first section from the above command, how do I the second section in the same file? With 'useEffect'? How? Thanks for any helping.
input.js
import {useState} from "react";
import {validate} from './validators';
const INPUT_STATES = { /*this is not used!*/
UNTOUCHED: 'UNTOUCHED',
VALID: 'VALID',
INVALID: 'INVALID'
};
const myValidate= validate; /*validate is the my validation function*/
const Input = props => {
const [value,setValue] = useState('');
const [validate,setValidate] = useState(true);
const handleChange = (e) => {
setValue(e.target.value);
}
const handleValidate = () => {
const Validate = myValidate(value,props.validators);
setValidate(Validate);
}
return (
<div className={`form-input ${validate ? '' : 'form-input--invalid'}`} data-testid="form-input">
<label htmlFor={props.id}>{props.label}</label>
<input value={value} type={props.type} id={props.id} onChange={handleChange} onBlur={handleValidate} />
<p>{validate ? '' : props.errorText}</p>
</div>
)
};
export default Input;
I solved the above problem, by adding two If inside handleChange and handleValidate. Also, I defined another useState.
Also, I changed first parameter from myValidate, from value to e.target.value.
input.js:
import React from "react";
import {useState} from "react";
import {validate} from './validators';/*This file is cantains validation rulles.*/
const myValidate = validate; /*validate is the my validation function*/
const Input = props => {
const [value,setValue] = useState('');
const [validate,setValidate] = useState(true);
const [valid,setValid] = useState('VALID');
const handleChange = (e) => {
setValue(e.target.value);
if(valid === 'INVALID'){
handleValidate(e);
}
}
const handleValidate = (e) => {
const Validate = myValidate(e.target.value,props.validators);
setValidate(Validate);
if(!Validate){
setValid('INVALID');
}
}
return (
<div className={`form-input ${validate ? '' : 'form-input--invalid'}`} data-testid="form-input">
<label htmlFor={props.id}>{props.label}</label>
<input value={value} type={props.type} id={props.id} onChange={handleChange} onBlur={handleValidate} />
<p>{validate ? '' : props.errorText}</p>
</div>
)
};
export default Input;
Now:
An error is not displayed during the first focus on input until taken focus from it. If there is an error, it displays. and next times, during of the changing if encounter with an error, the same time displays.

React form submission logic with lifted state and controlled dependence

I've dug myself into a deep rabbit hole with this component in an attempt to use React hooks.
The Parent component handles a dictionary state which is eventually distributed to multiple components.
My problem child component WordInput has a form with a single input. When submitting the form the component is fetching the word's definition from an API and passing on both the word and the definition to the parent which then sets the state in the form of dictionary. So far, so good IF it's the first word in dictionary. The part I'm having trouble with is to submit any subsequent words/definitions.
When the user submits a subsequent word, I want the component to check whether the word already exists in the dictionary that is passed to the child. If it doesn't exist, add it to the dictionary via the submit function.
I think the problem is that I'm trying to do too much with useEffect
I useEffect to:
- set loading
- check and process the dictionary for existing words
- check that definition and word aren't empty and submit both to parent/dictionary
- fetch a definition from an API
In the unprocessed code, I have multiple console.groups to help me keep track of what is happening. The more I add to the component, the more Subgroups and subgroups of subgroups accumulate. Clearly, the approach I'm taking isn't very dry and causes too many re-renders of the component/useEffect functions. For conciseness, I have taken out the console.log entries.
The imported fetchWordDefinition merely processes the fetched data and arranges it correctly into an array.
I don't know how to keep this dry and effective, and any help is appreciated with this rather simple task. My hunch is to keep all the logic to submit the word/definition in the submit handler, and only use useEffect to validate the data prior to that.
import React, { useState, useEffect } from "react";
import fetchWordDefinition from "./lib/utils";
const WordInput = ({ onSubmit, dictionary }) => {
const [definition, setDefinition] = useState([]);
const [cause, setCause] = useState({ function: "" });
const [error, setError] = useState({});
const [loading, setLoading] = useState(false);
const [word, setWord] = useState("");
const [wordExistsInDB, setWordExistsInDB] = useState(false);
useEffect(() => {
const dictionaryEmpty = dictionary.length === 0 ? true : false;
if (dictionaryEmpty) {
return;
} else {
for (let i = 0; i < dictionary.length; i += 1) {
if (dictionary[i].word === word) {
setWordExistsInDB(true);
setError({ bool: true, msg: "Word already exists in DB" });
break;
} else {
setWordExistsInDB(false);
setError({ bool: false, msg: "" });
}
}
}
}, [dictionary, word]);
useEffect(() => {
const definitionNotEmpty = definition.length !== 0 ? true : false;
const wordNotEmpty = word !== "" ? true : false;
if (wordNotEmpty && definitionNotEmpty && !wordExistsInDB) {
onSubmit(word, definition);
setWord("");
setDefinition([]);
}
}, [definition, word, onSubmit, wordExistsInDB]);
useEffect(() => {
if (cause.function === "fetch") {
async function fetchFunction() {
const fetch = await fetchWordDefinition(word);
return fetch;
}
fetchFunction().then(definitionArray => {
setDefinition(definitionArray);
setCause({ function: "" });
});
}
}, [cause, word]);
const handleSubmit = async e => {
e.preventDefault();
setLoading(true);
setCause({ function: "fetch" });
};
return (
<form onSubmit={handleSubmit}>
{error.bool ? <span>{error.msg}</span> : null}
<input
name='word'
placeholder='Enter Word'
type='text'
value={word}
onChange={({ target: { value } }) => setWord(value)}
/>
<input type='submit' />
</form>
);
};
export default WordInput;
There are indeed more useEffect's happening than necessary, as well as most of the state. All you need is the handleSubmit to do the fetching.
const WordInput = ({ onSubmit, dictionary }) => {
const [word, setWord] = React.useState("");
const handleChange = React.useCallback(e => {
setWord(e.currentTarget.value)
}, [])
const handleSubmit = React.useCallback(() => {
//check if word is in dictionary
const wordIsAlreadyThere = dictionary.map(entry => entry.word).includes(word)
//fetch the definition, wait for it, and call submit
if(!wordIsAlreadyThere && word.length > 0){
fetchWordDefinition(word)
.then(definition => {
onSubmit(word, definition)
setWord('')
}).catch(err => console.log(err))
}
}, [])
return (
<form onSubmit={handleSubmit}>
<input
value={word}
onChange={handleChange}/>
<input type='submit' />
</form>
);
}
I think you're missing out on some clarity and what useEffect is for
A functional component gets re-ran everytime either a prop or a state changes. useEffect runs when the component gets created, and we use it for things like doing a first-time fetch, or subscribing to an event handler. The second argument (array of variables) is used so that, if we have for example a blog post with with comments etc, we don't re-fetch everything unless the ID changes (meaning it's a new blog post)
Looking at your code, we have this flow:
User inputs something and hits Submit
Check if the word exists in a dictionary
a. If it exists, display an error message
b. If it doesn't exist, fetch from an API and call onSubmit
So really the only state we have here is the word. You can just compute an error based on if the word is in the dictionary, and the API call is done in a callback (useCallback). You have a lot of extra state that doesn't really matter in a state-way
A simplified version would look like this
const WordInput = ({ onSubmit, dictionary }) => {
const [word, setWord] = useState("")
const [loading, setLoading] = useState(false)
// `find` will find the first entry in array that matches
const wordExists = !!dictionary.find(entry => entry.word === word)
// Ternary operator,
const error = (wordExists) ? "Word already exists in DB" : null
// When user hits submit
const handleSubmit = useCallback(() => {
if (wordExists || !word.length) return;
setLoading(true)
fetchFunction()
.then(definitionArray => {
onSubmit(word, definitionArray)
})
}, [])
return (
<form onSubmit={handleSubmit}>
{error && <span>{error}</span>}
<input
name='word'
placeholder='Enter Word'
type='text'
value={word}
onChange={({ target: { value } }) => setWord(value)}
/>
<input type='submit' onclick={handleSubmit} disabled={wordExists}/>
</form>
);
};
Your component only needs to keep track of the word and the loading flag.
When the user changes the word input it updates the word state.
When the user submits the form the loading state changes. This triggers a useEffect that will first check if the word already exists. If not it proceeds to fetch it and add both the word and its definition to the dictionary.
const WordInput = ({ onSubmit, dictionary }) => {
const [loading, setLoading] = useState(false);
const [word, setWord] = useState("");
useEffect(() => {
if (!loading) return;
const existing_word = dictionary.find(item => item.word === word);
if (existing_word) return;
const fetchFunction = async () => {
const definition = await fetchWordDefinition(word);
// Update the dictionary
onSubmit(word, definition);
// Reset the component state
setWord("");
setLoading(false);
};
fetchFunction();
}, [loading]);
return (
<form
onSubmit={e => {
e.preventDefault();
if (word.length) {
setLoading(true);
}
}}
>
<input
name="word"
placeholder="Enter Word"
type="text"
value={word}
onChange={({ target: { value } }) => setWord(value)}
/>
<input type="submit" />
</form>
);
};
Please let me know if something is not clear or I missed something.

Categories

Resources