React.js - can't update form that is based on state - javascript

I'm trying to do a basic thing: insert data, which gets passed back to the main component, and display a list of items, that can be clicked and edited in the form.
Here's the relevant part of the code:
class SimpleForm extends React.Component {
constructor() {
super();
this.state = {
id: null,
firstName: ''
};
}
static getDerivedStateFromProps(props, state) {
if (props.user === null) return null;
return {
id: props.user.id,
firstName: props.user.firstName
}
}
handleChange = e => {
const value = e.target.value;
this.setState(prevState => {
return {
firstName: value
}
});
}
handleSubmit = e => {
e.preventDefault();
const event = e.target;
this.props.onAdd(this.state);
this.setState(prevState => {
return {
id: null,
firstName: ''
};
}, () => {
event.reset();
});
}
render() {
const {
firstName
} = this.state;
return ( <form onSubmit = {this.handleSubmit}>
<input type = "text"
name = "firstName"
value = {firstName}
onChange = {this.handleChange} />
<input type = "submit" value = "Submit" />
</form>
);
}
}
Here's the example: http://jsbin.com/yawasoz/1/edit?html,js,output
If you insert one item and then click on the "LI" element, you'll see that the state in the form gets set properly. However, you can't edit the data at all in the input - when I'm typing, the text stays the same. Much like if the "onChange" method didn't exist. What's going on here? I think that I might be using "getDerivedStateFromProps" incorrectly?

Apparently, this is a bug (as stated here https://www.reddit.com/r/reactjs/comments/8mslha/cant_update_form_that_is_based_on_state/dzq4zen) in the latest version of React, downgrading to 16.3 works as intended: http://jsbin.com/wilahihuyu/1/edit
<script crossorigin src="https://unpkg.com/react#16.3/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16.3/umd/react-dom.development.js"></script>
EDIT
Based on the discussion on github, this is NOT a bug, the bug was in the code and the version 16.4 made it apparent. Here's the discussion: https://github.com/facebook/react/issues/12898#issuecomment-392035163

Related

ReactJS Classes How to pass State from Child Component to its Parent?

I will call the LI tags like this (li) so they are not made into bullet points for his question
Hi I am trying to send a Child component to its Parent in ReactJS.
I tried many things I managed to send the child component state back up to its Parent props but when the page renders I have a couple of (li) tags which I want the update to update with it for example like:
(li) hard coded text (/li)
(li) old text (/li)
(li) update prop (/li)
(li) update prop etc (/li)
but instead the update deletes all previous code so it looks like:
(li) update prop deleted all previous li's (/li)
Hope that made sense here is my code
Parent Component
import React from 'react';
import { generateId, getNewExpirationTime } from '../../util/utilities';
import Thought from '../Thought/Thought.js'
import AddThoughtForm from '../AddThoughtForm/AddThoughtForm.js'
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
thoughts: [{
id: generateId(),
text: 'This is a place for your passing thoughts.',
expiresAt: getNewExpirationTime()
},
{
id: generateId(),
text: "They'll be removed after 15 seconds.",
expiresAt: getNewExpirationTime()
}]
};
this.addThought = this.addThought.bind(this);
this.removeThought = this.removeThought.bind(this);
this.componentDidMount = this.componentDidMount.bind(this);
this.componentDidUpdate = this.componentDidUpdate.bind(this);
}
addThought(thought) {
console.log("LOOK")
console.log(thought)
console.log("DONE")
console.log(this.state.thoughts);
this.state.thoughts.push(thought);
this.setState(prevState => ({
...prevState,
thoughts: [thought]
}))
console.log("passed over")
console.log(this.state.thoughts);
}
removeThought(selected) {
//alert('selected');
let updatedThoughts = this.state.thoughts.filter((thought) => selected.id !== thought.id);
return this.setState({ thoughts: updatedThoughts })
}
render() {
return (
<div className="App">
<header>
<h1>Passing Thoughts</h1>
</header>
<main>
<AddThoughtForm
addThought={this.addThought}
thoughts={this.state.thoughts} />
<ul className="thoughts">
{(this.state.thoughts) && this.state.thoughts.map((thought, index) => (
<Thought
key={thought.id}
thought={thought}
removeThought={this.removeThought} />
))}
</ul>
</main>
</div>
);
}
}
Still on the Parent Component my .addThought(arg) is where the action is. This is where I'm sending the Child AddThoughtForm state object into it. By inside of .addThought() I am doing this:
addThought(thought) {
console.log("LOOK")
console.log(thought)
console.log("DONE")
console.log(this.state.thoughts);
this.state.thoughts.push(thought);
this.setState(prevState => ({
...prevState,
thoughts: [thought]
}))
console.log("passed over")
console.log(this.state.thoughts);
}
What happens is when I pass it over my previous State of my parent is deleted and replaced by this new information from my child component. How do I stop that? I want to only add this new Information to the previous info that the Parent state already have. here is the state from my parent:
constructor(props) {
super(props);
this.state = {
thoughts: [{
id: generateId(),
text: 'This is a place for your passing thoughts.',
expiresAt: getNewExpirationTime()
},
{
id: generateId(),
text: "They'll be removed after 15 seconds.",
expiresAt: getNewExpirationTime()
}]
};
this.addThought = this.addThought.bind(this);
this.removeThought = this.removeThought.bind(this);
this.componentDidMount = this.componentDidMount.bind(this);
this.componentDidUpdate = this.componentDidUpdate.bind(this);
}
Now over to the Child Component
AddThoughtForm.js
import React from 'react';
import { generateId, getNewExpirationTime } from '../../util/utilities';
class AddThoughtForm extends React.Component {
constructor(props) {
super(props);
this.state = {
ideas: [this.props.thoughts] // I can take off []
}
this.handleTextChange = this.handleTextChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleTextChange(event) {
const { value } = event.target
this.setState(prevState => {
let thoughts = Object.assign({}, prevState.ideas); // creating copy of state variable
thoughts.id = generateId();
thoughts.text = value; // update the name property, assign a new value
thoughts.expiresAt = getNewExpirationTime();
return { thoughts }; // return new object
})
console.log(this.state.ideas)
}
handleSubmit(event) {
event.preventDefault();
this.props.addThought(this.state.thoughts)
alert(this.state.ideas);
}
render() {
return (
<form className="AddThoughtForm" onSubmit={this.handleSubmit}>
<input
type="text"
aria-label="What's on your mind?"
placeholder="What's on your mind?"
value={this.state.text}
onChange={this.handleTextChange}
/>
<input type="submit" value="Add" />
</form>
)
}
}
export default AddThoughtForm;
On my .handleTextChange(event) is where I am linking it with my input tag in render so what I am doing what ever Is typed into it I want entered I want it be passed to my Parent Component. well It is passed over but it over-writes the old info every time the old (li) and it is then all just a new li being rendered. Any ideas on how I can fix all of this?
handleTextChange(event) {
const { value } = event.target
console.log(value)
this.setState(prevState => {
let thoughts = Object.assign({}, prevState.ideas); // creating copy of state variable
thoughts.id = generateId();
thoughts.text = value; // update the name property, assign a new value
thoughts.expiresAt = getNewExpirationTime();
return { thoughts }; // return new object
})
console.log(this.state.ideas)
}
I managed to fix it
just had to add on the Parent Component a thoughts: [...prevState, thought] as I was overwritting the old thoughts with the new incoming thought
Like this:
In the method, .addThought()
this.setState(prevState => ({
...prevState,
thoughts: [...prevState.thoughts, thought]
}))

React state not changing

I have a react component like this:
const students: [
{id: '', name: '', age: '', attendance: ''}
//... 1000 entries
]
class Students extends React.Component {
constructor (props) {
super(props)
this.state = {
studentID: 1
}
}
createMonthlyChart = () => {
const { studentID } = this.state
let computeScore = students.attendance.map (
item => {
//... get attendance by id
}
)
return chartData
}
handleOnChange = value => {
console.log(value) // student key
this.setState({
studentID: value
})
this.createMonthlyChart() // not working
}
render () {
return (
<div>
// ant design component
<Select
defaultValue={type}
onChange={this.handleOnChange}
>
students.map((student, key) => (
<Option key={student.id}> {student.name} </Option>
))
</Select>
</div>
)
}
}
That is the just idea
I am not sure if I am using setState wrongly but somehow I get unexpected value
For example, the first time I click on a student, I don't get any chart visualization even though the data is there, I have to press it second time to get any chart.
And If I click on student without any attendance, I get empty chart after that for all students. I have to refresh the page to restart
To me it seems like you don't need the studentID state at all, you could directly call this.createMonthlyChart passing the value as a parameter to the function.
But if you need it for some other reason you can invoke that function as a callback to the setState like this:
this.setState({
studentID: value
}, this.createMonthlyChart)
I see a couple of things
The option should have the value
<option key={student.id} value={student.id}> {student.name}</option>
createMonthlyChart, should be called after updating the state (second parameter)
And you should use event.target.value
handleOnChange = event => {
this.setState({
studentID: event.target.value,
}, this.createMonthlyChart);
};
And for the first time, you can use componentDidMount
componentDidMount() {
this.createMonthlyChart();
}
And don't forget to initialize the state with the first student, like this
this.state = {
studentID: students[0].id,
};

React setState not immediately updating component view

I have a input field that is filtering the elements on of an array.
The search results are always one keystroke behind, I assume because setState doesn't instantly update the view? What's the best way to work around that?
class App extends Component {
constructor() {
super();
this.state = {
images:[],
searchfield: '',
filteredImages:[],
suggestedKeywords:[],
inputValue: ''
}
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value});
this.setState({inputValue: event.target.value});
let filteredImages = this.state.images.filter(image => {
return image.labels.includes(this.state.searchfield.toLowerCase());
});
console.log(event.target.value);
this.setState({filteredImages});
}
}
const SearchBox = ({searchfield, searchChange, inputValue}) => {
return (
<div>
<input
type="search"
value={inputValue}
onChange={searchChange}
placeholder="Search images..."
/>
</div>
);
}
The search results are always one keystroke behind, I assume because setState doesn't instantly update the view? What's the best way to work around that?
That isn't the problem.
Your problem is that you are assuming updates to setState occur instantly.
this.setState({searchfield: event.target.value}); //You update searchfield here
return image.labels.includes(this.state.searchfield.toLowerCase());
//but this.state.searchfield doesn't reflect the update yet!
So instead, simply work off of the updated value rather than the one from the store.
return image.labels.includes(event.target.value.toLowerCase());
setState is asynchronous, so you will be filtering by the old searchfield when you do this.state.searchfield.toLowerCase(). With this in mind you could do something like this instead:
onSearchChange = (event) => {
const { value } = event.target;
const newState = {
searchField: value,
inputValue: value
};
newState.filteredImages = this.state.images.filter(image => {
return image.labels.includes(value.toLowerCase());
});
this.setState(newState);
}

Keep getting a controlled vs uncontrolled react error

I keep getting this error in my console:
warning.js?8a56:36 Warning: LoginForm is changing a controlled input
of type password 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. More info: https://facebook.github.io/react/docs/forms.html#controlled-components
I have had a look at these questions:
How to create a controlled input with empty default in React 15 - I define my state already
React Controlled vs Uncontrolled inputs - My function takes an event and not a password parameter
React - changing an uncontrolled input - I already define the password as empty
Here is the component:
export default class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {
values: {
username: "",
password: ""
}
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this._clearInput = this._clearInput.bind(this);
}
onSubmit(event) {
// this.props.attemptLogin
event.preventDefault();
console.log(this.state.values);
let {username, password} = this.state.values;
this.props.attemptLogin(username, password)
.then((userInfo) => {
console.log(userInfo);
LocalStore.setJson('api', userInfo.api);
LocalStore.setJson('user', userInfo.user);
this._clearInput('username' , 'password');
})
.catch((err) => {
console.log("Failed:");
console.log(err);
this._clearInput('password');
});
}
_clearInput(...fields) {
let newValuesState = {};
fields.forEach((field) => {
newValuesState[field] = '';
});
this.setState({values: newValuesState});
}
onChange(event) {
let name = event.target.name;
let value = event.target.value;
this.setState({values: {[name]: value}});
}
render() {
return (
<div>
<form method="post" onSubmit={this.onSubmit}>
<div><input type="text" name="username" onChange={this.onChange} value={this.state.values.username}/></div>
<div><input type="password" name="password" onChange={this.onChange} value={this.state.values.password}/></div>
<div><button type="submit">Login</button></div>
</form>
</div>
);
}
}
LoginForm.propTypes = {
attemptLogin: React.PropTypes.func.isRequired
};
The code does seem to work but this error pops up in the console and so I can't continue I till I get it to stop appearing. Can anyone see what's I did wrong in the component?
Since you are nesting the values in state.values, your onChange function will remove the field not being changed. You could do something like this instead:
onChange(event) {
let name = event.target.name;
let value = event.target.value;
let values = {...this.state.values};
values[name] = value;
this.setState({values}});
}

Notify react components about value change

Suppose that I have a component class which is responsible to change any number entered into textbox to text:
class NumbersToText extends Component {
onChange(event) {
const { target } = event;
const { value } = target;
if (hasNumbers(value)) {
target.value = numbersToText(value);
// HERE I NEED TO NOTIFY ABOUT CHANGES
}
}
render() {
return (
<span onChange={this.onChange}>
{this.props.children}
</span>
);
}
}
Now the usage would look something like this:
<NumbersToText>
<input onChange={this.saveValue}
</NumbersToText>
Let's say that all works, and the value gets changed to text.
Now the problem is that after I change numbers to text and assign that value to input onChange handlers are not executed again, thus saveValue is not called with updated value.
How should this problem be approached in order to trigger onChange handlers with new value?
I don't know exactly what you mean by numbers to text so I'll just assume you want to modify the value before calling the onChange function in the input, and also reflect that value in the input.
First of all, what you're doing will never work on React, React reflects internal virtual objects into the DOM, meaning you shloud not modify the DOM directly and instead you should modify this internal representantion (via setState, props) to reflect this change into the DOM.
There's also two types of inputs on React, controlled and uncontrolled. I will assume you want to use this on uncontrolled inputs.
The only possible solution I can see, is to transform the input using the React.cloneElement function adding a aditional step before calling the input's onChange callback.
Here's a possible implementation that will make the input uppercase.
class UpperCase extends React.Component {
constructor(props) {
super(props);
}
onChange(e, input, next) {
let value = input.value || '';
value = value.toUpperCase();
input.value = value;
next(value);
}
render() {
let childs = React.Children.map(this.props.children, child => {
let input = null; //Will take advantage of javascript's closures
let onChangeChild = child.props.onChange.bind(child);
return React.cloneElement(child, {
ref: ref => input = ref,
onChange: e => {
this.onChange(e, input, onChangeChild)
}
});
});
return (
<span>
{childs}
</span>
);
}
}
And you can use it like this:
<UpperCase>
<input onChange={(val) => console.log(val)}></input>
<textarea onChange={(val) => console.log(val)}></textarea>
</UpperCase>
Thanks to #tiagohngl I came up with a similar, but maybe a little less cluttered (without cloning elements) way:
class NumbersToText extends Component {
onChange(event) {
const { target } = event;
const { value } = target;
if (hasNumbers(value)) {
target.value = numbersToText(value);
this.childrenOnChange(event);
}
}
childrenOnChange(event) {
const { children } = this.props;
React.Children.forEach(children, child => {
if (child.props.onChange) {
child.props.onChange(event);
}
});
}
render() {
return (
<span onChange={this.onChange}>
{this.props.children}
</span>
);
}
}
export default class NumbersToText extends React.Component {
constructor(props) {
super(props)
this.onChange = this.onChange.bind(this);
}
componentWillMount() {
this.setState({ anyData: [] });
}
onChange(event) {
this.setState({anyData: event.target.value},
()=>{console.log("AnyData: "+this.state.anyData)});
// callback to console.log after setState is done
}
render() {
return (
<input type="text"
value={this.state.anyData}
onChange={this.onChange} />
);
}
}
As you mention that,
onChange is not called after changed value.
There are multiple possibilities.
onChange is not binded.
There are no state change in render method, so it will not re-render
make use of console.log() to trace the problem
I slightly ammend the code for illustration.
Hope it helps.
How react handle State Change (answer I posted before)

Categories

Resources