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.
Related
This question already has answers here:
How to use `setState` callback on react hooks
(22 answers)
Closed 2 years ago.
There are 2 event handlers that handle passwords.
The first inputPassword writes the value from its input to state
the second inputPasswordRE writes the value of input and compares it to the value from inputPassword.
Since setState works asynchronously, even if the same values are entered, the check fails, so
how in inputPasswordRE its previous state is compared (When you enter in the password field - 12345 and in the re_password field - 12345, their values password === re_password will be false.).
How to correctly write setState in inputPasswordRE so that
did the comparison work correctly?
const inputPassword = (e) => {
setState(({ ...state, password: e.target.value }));
};
const inputPasswordRE = (e) => {
setState({ ..state, re_password: e.target.value });
if (password === re_password) {
alert(`SUCCESS`)
} else {alert(`ERROR`)}
};
You're right, setState is updates state asynchronously, so it does not guaranty change immediately, you can use following approach to do your task.
Using Class based
const inputPasswordRE = (e) => {
this.setState({ ..state, re_password: e.target.value }, (state) => {
if (state.password === state.re_password) {
alert(`SUCCESS`)
} else {
alert(`ERROR`)
}
});
};
OR
You can also use `componentDidUpdate` as well.
Using React Hooks
If you're using react hooks, you can use useEffect to detect the changes and do your validations.
useEffect(() => {
if (password === re_password) {
// Do your stuff here
}
}, [password, re_password])
You can use the following for class-based components.
componentDidUpdate(prevProps, prevState) {
if (this.state.password === this.state.rePassword) {
// passwords matched
} else {
// passwords didn't match
}
}
You can also use function-based component with a useState hook.
Check out the following code
import React, {useState, useEffect} from 'react'
export default function func() {
const [password, setPassword] = useState('');
const [rePassword, setRePassword] = useState('');
useEffect(() => {
if (password === rePassword){
// passwords matched
} else {
// passwords don't match
}
})
return (
<div>
<input type="text" name="password" type="password" onClick={(e) => setPassword(e.target.value)} />
<input type="text" name="password" type="password" onClick={(e) => setRePassword(e.target.value)} />
</div>
);
}
This should work perfectly.
Comment if you wanna know anything else.
Good day to all:
I am trying to write jest test cases for the below component, but cannot figure out how to write a test for the line "if (value === valueRef.current.value)". How do I mock a value to be different from reference current value?
Thank you.
const Input = ({
setEnteredValueHandler
}) => {
const [value, setValue] = useState(defValue);
const valueRef = useRef();
useEffect(() => {
const timer = setTimeout(() => {
if (value === valueRef.current.value) {
setEnteredValueHandler(value);
}
}, 500);
return () => {
clearTimeout(timer);
};
}, [value]);
return (
<input
value={value}
ref={valueRef}
onChange={e => setValue(e)}
/>
);
};
export default Input;
By now your component has logical error: value={value} onChange={e => setValue(e)}. It sets Event into value. I believe it should be onChange={e => setValue(e.target.value)}.
Next about component. By now the only way to get value and valueRef.current.value different is to change latter one programmatically. Any other interaction(keypress or input) will call onChange handle and set value equal to valueRef.current.value.
Also we need mock timer to avoid waiting for real 500ms.
test('does not call callback if value has been changed programmatically during 500ms', () => {
jest.useFakeTimers();
const setEnteredValueHandler = jest.fn();
const { getByRole } = render(<Input setEnteredValueHandler={setEnteredValueHandler} />);
const input = getByRole('textbox');
fireEvent.change(input, { target: { value: "aaa" } });
input.value = "aa";
expect(setEnteredValueHandler).not.toHaveBeenCalled();
jest.advanceTimersByTime(502);
expect(setEnteredValueHandler).not.toHaveBeenCalled();
});
It's for React-Testing-Library instead of Enzyme, but it should be easier to adapt.
And related test for case when value has not been changed programmatically so callback is called after 500ms.
test('calls callback if value has not been changed programmatically during 500ms', () => {
jest.useFakeTimers();
const setEnteredValueHandler = jest.fn();
const { getByRole } = render(<Input setEnteredValueHandler={setEnteredValueHandler} />);
const input = getByRole('textbox');
fireEvent.change(input, { target: { value: "aaa" } });
expect(setEnteredValueHandler).not.toHaveBeenCalled();
jest.advanceTimersByTime(499);
expect(setEnteredValueHandler).not.toHaveBeenCalled();
jest.advanceTimersByTime(2);
expect(setEnteredValueHandler).toHaveBeenCalledWith("aaa");
});
on first button press onChange method function is called but state is not updating as it should and on second button press it is updating see this
import React,{useState} from 'react';
function MainHeader(props) {
const [FirstName, setFirstName] = useState('')
const [User, setUser] = useState({
FirstName: '',
LastName: ''
})
const nameOnChange = (event) => {
setFirstName(event.target.value)
console.log(FirstName)
}
const addName = () => {
setUser({
...User,
FirstName: FirstName
})
console.log(User)
props.addUserToFirebase(User)
}
return (
<div>
<h1>Checking</h1>
<input type="text" onChange={(e) => nameOnChange(e)} value={FirstName} />
<button onClick={() => addName()}>Enter</button>
</div>
);
}
on nameOnChange it is console.log(FirstName) when first time a pressed something then console logs empty state (initial state) and on second button it updates the previous button pressed. I have tried creating class component as well but i am seeing the same issue , same thing happens in the addName function it updates state on second click .
see console
I don't see a problem here. It is working perfectly as it should be. But the only problem I see is your wrong understanding of how React works or how Functional Programming works in general.
There's no mutation in Functional Programming
const nameOnChange = (event) => {
// event => new value
// FirstName => old value
// they remain that way throughout this function call
setFirstName(event.target.value)
// even if you set the state, the values won't change
// they will be updated only in next function call
console.log(FirstName) // still old value
}
The same goes for addName()
For each re-render React call the function MainHeader with values that will not be mutated throughout their call or life. When value are updated, React will call MainHeader with the updated the values.
Correct way of using your Component
Works, but not better way
const addName = () => {
setUser({
...User,
FirstName: FirstName
})
//
console.log({
...User,
FirstName: FirstName
}) // new value, since User is not mutated, User will still have the old value
props.addUserToFirebase(({
...User,
FirstName: FirstName
})
}
Better way
Always use useEffects for side effects.
// Just set the state
const addName = () => {
setUser({
...User,
FirstName: FirstName
})
}
// handle side effects here
useEffect(() => {
// check is needed here, since it will be
// called on component's first mount
if(User.FirstName !== ''){
console.log(User)
props.addUserToFirebase(User)
}
}, [User])
// This will be called whenever React detects a change in `User`
https://reactjs.org/docs/hooks-effect.html
You must use useEffect().
const nameOnChange = (event) => {
setFirstName(event.target.value)
}
useEffect(()=>{
if(FirstName !== ''){
console.log(FirstName)
}
}, [FirstName])
const addName = () => {
setUser({
...User,
FirstName: FirstName
})
}
useEffect(()=>{
if(User.FirstName !== '' && User.LastName !== ''){
console.log(User)
props.addUserToFirebase(User)
}
}, [User])
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.
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}