Child Stateless Functional Component doesn't update even after parent State updates - javascript

My parent component has a property in its state called formIsValid, initially set to false. My form also has a submit button. I want the submit button to be disabled until after some input fields (a first name and a last name input) have data in them.
This is what my state looks like:
state = {
employees: [],
costEstimates: emptyCosts(),
relationshipOptions: [],
newEmployee: emptyEmployee(),
formIsValid: false
};
This function handles changes to the First and Last name inputs:
// handle input into "First Name" and "Last Name" inputs
handleChangeValue = async e => {
const newEmployee = { ...this.state.newEmployee };
newEmployee[e.currentTarget.name] = e.currentTarget.value;
this.setState({ newEmployee });
this.validateIfCanBeSubmitted();
await this.updateCostsData(newEmployee); // this is an api thing, not relevent
};
This is what sets the formIsValid property in the state. This property is sent as a prop to the Submit button.
validateIfCanBeSubmitted = () => {
const { firstName, lastName } = this.state.newEmployee;
let formIsValid = firstName && lastName ? true : false;
this.setState({ formIsValid });
};
The Submit button for this is correctly getting disabled if the employee property in the state has its first and last names as empty. The problem is that it's "off by 1 update." It's as if the props aren't getting propagated down to the child button component until after the NEXT time the state changes. Here's a gif of the issue:
This is what the child component looks like. It's just a regular HTML button, however it's within a Stateless Functional Component, so the issue is not with the component's state:
<button
type="button"
onClick={onSubmit}
className={'btn btn-primary mr-1 ' + (formIsValid ? '' : 'disabled')}
disabled={!formIsValid}
>

setState() is asynchronous!
this.validateIfCanBeSubmitted(); is executed on the old state; this update this.setState({ newEmployee }); has not been propagated to this.state when your function is executed.
Make validateIfCanBeSubmitted an update-function.
validateIfCanBeSubmitted = ({ newEmployee: { firstName, lastName }}) => {
return {
formIsValid: firstName && lastName ? true : false
};
}
and use it accordingly:
handleChangeValue = async e => {
const {name, value} = e.currentTarget;
const newEmployee = {
...this.state.newEmployee,
[name]: value
};
this.setState({ newEmployee });
this.setState(this.validateIfCanBeSubmitted);
// this is an api thing, not relevant
await this.updateCostsData(newEmployee);
};
Actually, the code in handleChangeValue should also be in such a function, as it uses the previous state to compute the new one.
so how about combining them:
handleChangeValue = e => {
const {name, value} = e.currentTarget;
this.setState((state) => {
const newEmployee = {
...this.state.newEmployee,
[name]: value
};
const { firstName, lastName } = newEmployee;
const formIsValid = firstName && lastName ? true : false;
//and since you never use the returned Promise, why make anything async?
this.updateCostsData(newEmployee);
return { newEmployee, formIsValid };
});
};

Related

React Jsx set checked state to false (reset button)

Here I'm trying to reset selected radio buttons on this list,
however it doesn't work because
I previously change input check from {checked} to {user.checked}. Refer from UserListElement.tsx below
Therefore, I tried the following two methods.
in useEffect(), set user.userId = false
useEffect(() => {
user.checked = false;
}, [isReset, user]);
→ no change.
setChecked to true when addedUserIds includes user.userId
if (addedUserIds.includes(`${user.userId}`)) {
setChecked(true);
}
→ Unhandled Runtime Error
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
Any suggestion on how to make this this work?
UserListElement.tsx
export const UserListElement = ({
user,
handleOnMemberClicked,
isReset,
}: {
user: UserEntity;
handleOnMemberClicked: (checked: boolean, userId: string | null) => void;
isReset: boolean;
}) => {
const [checked, setChecked] = useState(user.checked);
const addedUserIds = addedUserList.map((item) => item.userId) || [];
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
const checkedState = e.target.checked;
setChecked(checkedState); //not called
user.checked = checkedState;
handleOnMemberClicked(checkedState, user.userId);
};
useEffect(() => {
setChecked(false);
}, [isReset, user]);
if (addedUserIds.includes(`${user.userId}`)) {
user.checked = true;
// setChecked(true) cause runtime error (infinite loop)
}
return (
<li>
<label className={style.checkboxLabel}>
<input
type="checkbox"
className={style.checkboxCircle}
checked={user.checked}
// checked={checked}
onChange={(e) => handleOnChange(e)}
/>
<span>{user.name}</span>
</label>
</li>
);
};
UserList.tsx
export const UserList = (props: {
showsUserList: boolean;handleClose: () => void;corporationId: string;currentUserId: string;samePerson: boolean;twj: string;
}) => {
const [isReset, setReset] = useState(false);
.......
const resetAll = () => {
setReset(!isReset);
setCount((addedUserList.length = 0));
setAddedUserList([]);
setUserName('');
};
......
return ( <
> < div > xxxxx <
ul className = {
`option-module-list no-list option-module-list-member ${style.personListMember}`
} > {searchedUserList.map((user, i) => (
<UserListElement user = { user }
handleOnMemberClicked = { handleOnMemberClicked }
isReset = { isReset }
key = {i} />
)) }
</ul>
/div>
<a className="is-secondary reservation-popup-filter-reset" onClick={resetAll}>
.....
}
UseAddUserList.tsx
export class UserDetail {
constructor(public userId: string | null, public name: string | null) {}
}
export let addedUserList: UserDetail[] = [];
export let setAddedUserList: Dispatch<SetStateAction<UserDetail[]>>;
export const useAddUserList = (idList: UserDetail[]) => {
[addedUserList, setAddedUserList] = useState(idList);
};
Further Clarification:
Default view
Searched option (showed filtered list)
I use user.checked because when using only checked, the checked state does not carry on from filtered list view to the full view (ex. when I erase searched word or close the popup).
The real answer to this question is that the state should NOT be held within your component. The state of checkboxes should be held in UsersList and be passed in as a prop.
export const UserListElement = ({
user,
handleOnMemberClicked,
isChecked
}: {
user: UserEntity;
handleOnMemberClicked: (checked: boolean, userId: string | null) => void;
isChecked: boolean;
}) => {
// no complicated logic in here, just render the checkbox according to the `isChecked` prop, and call the handler when clicked
}
in users list
return searchedUserList.map(user => (
<UserListElement
user={user}
key={user.id}
isChecked={addedUserIds.includes(user.id)} <-- THIS LINE
handleOnMemberClicked={handleOnMemberClicked}
/>
)
You can see that you almost had this figured out because you were doing this in the child:
if (addedUserIds.includes(`${user.userId}`)) {
user.checked = true;
// setChecked(true) cause runtime error (infinite loop)
}
Which indicates to you that the checkdd value is entirely dependent on the state held in the parent, which means there is actually no state to be had in the child.
Also, in React, NEVER mutate things (props or state) like - user.checked = true - that's a surefire way to leave you with a bug that will cost you a lot of time.
Hopefully this sheds some light
In your UserListElement.tsx you are setting state in render, which triggers renders the component again, and again set the state which again triggers re-render and the loop continues. Try to put your condition in the useEffect call, also you mutate props, so don't set user.checked = true. Instead call setter from the parent component, where it is defined.
useEffect(() => {
setChecked(false);
if (addedUserIds.includes(user.userId)) {
setChecked(true);
}
}, [user]);

on first button press state is not updating but function called

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])

remove empty strings in an array before this.setState

Initial state
super(props);
this.state = {
user: {
id: 0,
firstName: '',
lastName: '',
phoneNumbers: ['', '', ''],
}
};
}
my handleInputChange function:
handleInputChange = (event) => {
const { user } = this.state;
const name = event.target.name;
const value = event.target.value;
if (name === 'contactNumber1') {
user.phoneNumbers[0] = value;
} else if (name === 'contactNumber2') {
user.phoneNumbers[1] = value;
} else if (name === 'contactNumber3') {
user.phoneNumbers[2] = value;
console.log('hellooo', user.phoneNumbers);
} else {
user[name] = value;
}
const filteredPhoneNumbers = user.phoneNumbers.filter(e => e !== '');
this.setState(prevState => ({
user: {
...prevState.user,
[name]: value,
phoneNumbers: [...filteredPhoneNumbers],
},
}));
}
When I remove an element, the element in the index above populates the element I just deleted What am I missing?
* I am getting this error because of my code too - A component is changing a controlled input of type undefined to be uncontrolled. Input elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component*
You could try parsing "this.state" like this:
const { user } = JSON.parse(JSON.stringify(this.state));
I think you're referencing the same object to the user cons

ReactJS - Get data from multiple TextField (unknown quantity) then setState

I have a list of columns, they are rendered as TextFields:
{columns.map((column, index) => (
<TextField
key={index}
margin="dense"
id={column}
label={column}
name={column}
type="text"
onChange={this.handleInputChange}
fullWidth/>
))}
and handleInputChange function is written as below:
handleInputChange(event) {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({
addData: {
[this.state.fullName]:{
[name]: value
}
}
});
}
The data that I want to receive is:
addData: {
[name-of-column-1]: value,
[name-of-column-2]: value,
[name-of-column-3]: value,
.......
}
but the handleInputChange function overrides after every TextField change, the data that I received:
addData: {
[name-of-column-1 (or 2, 3...)]: value,
}
Is there any way to get the data I need? Thanks everyone!
How do I setState for nested object?
handleInputChange(event) {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({
addData: {
[this.state.fullName]:{
...this.state.addData[this.state.fullName],
[name]: value,
}
}
});
}
You are assigning whole new object every time value from one TextField changes and not retaining old values from state.
I'm not sure what exactly is this.state.fullName, but in order to have your desired state structure you can implement it like this:
handleInputChange(event) {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({
addData: {
...this.state.addData,
[name]: value
}
});
}
When destructing this.state.addData (with ...), you are basically assigning all of its properties to the new object and then adding additional [name]: value. Its important to put destruction above new assignment in order to update latest value.
I think you should prepare your data first and set it into your state or try in this way
this.setState({
addData: {
[...this.state.fullName]:{
[name]: value
}
}
});
This's a issue when you setState. When you using
this.setState({
addData: {
[this.state.fullName]:{
[name]: value
}
}
});
that mean you are setting a new state just have one nested object
addData { abc: value }
so the old values are lost. You need add them before you change the new value. Try this
handleInputChange(event) {
const { name, value } = event.target;
const newData = {...this.state.addData};
newData[name] = value;
this.setState({ addData: newData });
}
or
handleInputChange(event) {
const { name, value } = event.target;
this.setState({
addData: {
...this.state.addData,
[name]: value
}
});
}

How to set state from multiple Select components in one method

I've got two select components and I want to set values from this selectors to state using one handleChange method. I've tried to do like this:
handleChange = event => {
const { name, value } = event.target;
if (name === 'City') {
this.setState({ selectedCity: value });
} else if (name === 'Theatre') {
this.setState({ selectedTheatre: value });
}
};
But I want more universal solution, like I did with inputs:
handleChange = event => {
const { name, value } = event.target;
this.setState({
[name]: value
});
};
My state
this.state = {
cities: [],
theatres: [],
movies: [],
selectedCity: 1,
selectedTheatre: 1
};
at the first, the name for each input must look like the element in the state
ex
tell me
handaleChange=(event)=>{
this.setState({
[event.name]=event.target.value
}
}
tell me if it work or not
make Handel different for selected Combobox element different
handelSelectedElement=(event)=>{
const value=event.target.checked
const name:event.target.name;
let cityselected=Object.assign({},this.state.city,{[name]:value})
this.setState({city:cityselected})}
tell me if work or not
Ensure your dropdown list name matches the state name
this.state = {
cities: [],
theatres: [],
movies: [],
selectedCity: 1,
selectedTheatre: 1
};
<select name="selectedCity"
value={this.state.selectedCity}
onChange={this.handleChange }>
</select>
<select
name="selectedTheatre"
value={this.state.selectedTheatre}
onChange={this.handleChange }>
</select>
handleChange = event => {
const target = event.target
const value = target.type === 'checkbox' ? target.checked : target.value
const name = target.name
this.setState({
[name]: value
});
};
Note: Dont forget to copy your event data into a variable or call event.persist() to avoid issues.
See link [Event target is null inside functional setState

Categories

Resources