I have this constructor in React component:
constructor() {
super();
this.state = {
info: {
title: '',
description: '',
height: ''
}
}
...
And a form with inputs controlled by the state:
<form onSubmit={this.handleFormSubmit}>
<label>Title:</label>
<input type="text" name="title" value={this.state.info.title} onChange={(e) => this.handleChange(e)} />
<label>Description:</label>
<input type="text" name="description" value={this.state.info.description} onChange={(e) => this.handleChange(e)} />
...
When I type anything on the form, I guess there's something wrong with my handler, as I get the warning "Warning: A component is changing a controlled input of type text 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."
Checking console, it seems state is updating each property value that is being typed, and removing other properties, while it should remain all of them and only update the changed ones.
Here's my handler:
handleChange(event) {
let { name, value } = event.target;
this.setState({
info: {
[name]: value
}
});
}
the one you are using is updating only one props and the others being stripped that causes React shows warning. you can use spreading to keep the others
handleChange(event) {
let { name, value } = event.target;
this.setState({
info: {
...this.state.info,
[name]: value
}
});
}
There's usually 2 cases which cause such warnings to manifest in my experience.
The initial value of the property in state is undefined and not being changed when updated
The initial value is '' and being changed to null
Try this:
handleChange(event) {
let { name, value } = event.target;
this.setState(({info}) =>({
info: {
[name]: value
}
}));
}
Breaking down the function call:
First you have this.setState(updaterFunction). updaterFunction gets called by setState with the previous state as the argument, and that function is expected to return an object with the keys of the state to update (it gets merged shallowly with the previous state).
Because setState is only a shallow merge (no matter if you pass it an object or a function), if you have an object at like this.state.foo.bar and you update the state with an object like {foo: {bar: 'qux'} }, the old foo will not get merged with the new foo, it will instead get replaced. So your updater function needs to do the deeper merging manually.
The updaterFunction looks like this ({info})=>({…}). We pull info out of the previous state, and we return an object, using info to manually do the deeper merging.
The benefit of passing a function to setState (instead of just an object) is that if you use the state passed to the function instead of this.state, you will avoid some potential bugs when multiple setState calls get batched together....
Related
I am utilizing React-Bootstrap for a simple text box component. Right now, the callback is called every time the user edits the textbox, hence it is called back when every letter is called
<Form.Group controlId="Id">
<Form.Label>Id</Form.Label>
<Form.Control style = {{marginLeft: 10}} name="Idstate" onChange={this.handleChange} defaultValue = {this.state.idstate}/>
</Form.Group>
The code above is my form group
handleChange = (event) => {
event.persist()
const name = event.target.name
if (name == "Idstate"){
this.setState({idstate: event.target.value})
this.state.idstate = event.target.value;
Apiservice.updatetabletopmenudata(this.tableid, event.target.value, "idstate")
}
console.log(this.state)
}
This results in a really laggy process, and sometimes the data isn't saved properly
Is there any way for the callback to be executed only after the user has finished editing the text box?
This is because every time a state changes the component re-renders.So, you can avoid unnecessary renders by creating a variable in the state state = {fromOnChange: false} which should always be false and whenever there is a change in the input text field, handle like below
handleChange(event) {
this.setState({
fromOnChange: true,
});
}
And when the state updates keep a life cycle method that checks wheather to render the component or not :
shouldComponentUpdate(nextProps, nextState) {
if (!nextState.fromOnChange) {
return true;
} else {
return false;
}
}
After that whenever user had finished editing the text fields and submits the data then change the fromOnchange: false so that component renders only on onsubmit.
handleSubmit(event) {
//Your API/function call goes here
// and on success change the state so that it renders
this.setState({
fromOnChange: false
});
}
First, fixing errors before performance:
Using defaultValue would create an uncontrolled component - only the default value will be set from React, then ignore any state changes.
If you need to control the value from React, please use value:
<Form.Control ... value={this.state.idstate} />
Also, never modify state directly, this.setState({idstate: event.target.value}) is enough, no need for this.state.idstate = ... - but the operation is asynchronous (see the link above), so console.log(this.state) is meaningless inside the handleChange - you can console.log inside render or just use React DevTools to view the current state (both for classes and hooks).
Second, performance:
React state update is NOT slow by itself, it's pretty optimized. If you want to delay a slow Apiservice, you may try following:
handleChange = (event) => {
const name = event.target.name
const value = event.target.value
if (name == "Idstate"){
this.setState({idstate: event.target.value})
setTimeout(() =>
Apiservice.updatetabletopmenudata(this.tableid, value, "idstate")
)
}
}
I thought that context acts in a similar way as state so I've created function handleUpdate that can update state which is context using as a value. Afterwards I've noticed that context is being updated without triggering handleUpdate.
Provider:
<DashboardContext.Provider value={{dashboard:this.state,handleChange:this.handleChange}}>
{/*...*/}
</DashboardContext.Provider>
handleChange function
handleChange=(what, value)=> this.setState({[what]:value});
In another component which uses context: this triggers updating of context without calling handleUpdate.
let updatedTasks = this.context.dashboard.tasks;
updatedTasks[task.id] = {/* ... something */};
Afterwards it changes context value and parents state (which is context using) without calling setState. Is this usual behavior? I though that all states should be handled with setState function.
As the actual workaround to lose reference on contexts object I've used:
let updatedTasks = JSON.parse(JSON.stringify(this.context.dashboard.tasks));
but it doesn't seems like a correct solution for me.
Edit: as #Nicholas Tower suggested solution:
my current code
State in constructor now looks like this:
this.state = {
value: {
dashboard: {
// everything from state is now here
},
handleChange: this.handleChange,
}
};
I pass state.value instead of custom object now
<DashboardContext.Provider value={this.state.value}>
{/*...*/}
</DashboardContext.Provider>
but still when I do this, context and state (both) are being updated without calling handleChange
let updatedTasks = this.context.dashboard.tasks;
updatedTasks[task.id] = {/* ... something */};
The issue you have is in this part:
value={{dashboard:this.state,handleChange:this.handleChange}}
Every time your component renders (for whatever reason), this line will create a new object. The dashboard property and handleChange property may be unchanged, but the object surrounding them is always new. That's enough that every time it renders, the value changes, and so all descendants that use the value need to be rerendered too.
You'll need to modify your code so that this object reference does not change unless you want it to. This is typically done by putting the object into the component's state.
class Example {
handleChange = (what, value) => {
this.setState(oldState => ({
value: {
... oldState.value,
[what]:value
}
});
}
state = {
value: {
dashboard: {
// whatever you used to put in state now goes here
},
handleChange: this.handleChange
}
}
render() {
return (
<DashboardContext.Provider value={this.state.value}>
{/*...*/}
</DashboardContext.Provider>
)
}
}
You can see mention of this in React's documentation on context: https://reactjs.org/docs/context.html#caveats
Is the newer setState syntax not recommended for use with events?
While debugging, I saw that the first letter I typed came through with
a proper e.target.value.
Immediately afterwards though, I got the TypeError you see below.
// onChange(e) {
// this.setState(prevState => {
// return { username: e.target.value} <--- TypeError: Cannot read property 'value' of null
// })
// }
// obviously the setState below works fine
onChange(e) {
this.setState({ username: e.target.value});
}
<input type="text"
placeholder="What is your username?"
onChange={this.onChange}
// onChange={e => this.onChange(e)} - also tried this
value={this.state.username}/>
You are using the event in an asynchronous way by accessing it in the callback function you passed to setState(). By the time react calls your updater function all properties on the event have already been cleared.
From the react docs:
The SyntheticEvent is pooled. This means that the SyntheticEvent
object will be reused and all properties will be nullified after the
event callback has been invoked. This is for performance reasons. As
such, you cannot access the event in an asynchronous way.
To avoid that you either need to persist the event by calling event.perist(). This will enable you to use it later.
Or you assign the value you are interested in to a local variable that can be used asynchronously:
onChange(e) {
const username = e.target.value;
this.setState(prevState => ({username}));
}
Actually in your specific example you do not need the callback based version of setState() at all as you aren't updating the state based on previous state or props.
Update
I missed the error in the first place which is e.target is being null here. So, the real problem is not extracting the value from the event or using event.persist() there as #trixn explained in his/her answer. But, my answer includes this step though. So, it worked :)
Neither of them works if you don't bind your onChange function for this. Here is the working version of your code that you say does not work:
class App extends React.Component {
state = { username: "" };
onChange = (e) => {
const { value } = e.target;
this.setState(prevState => {
return { username: value}
})
}
render() {
return (
<div>
<input type="text"
placeholder="What is your username?"
onChange={this.onChange}
value={this.state.username} />
<p>Username: {this.state.username}</p>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
I just used an arrow function instead of implicitly binding it. So it is bond automatically. Also, I extracted the value from e.target for React's synthetic events handling. We can't use events properly if we use them in a callback function like in setState's here. This is why I extracted it then use as a separate variable.
Please note I'm Material-ui react js component for my Textfield. I have not used redux-forms.
I have declared the onchange listerner and state
constructor(props) {
super(props);
this.state = {
firstName: null,
};
this.handleInputChange = this.handleInputChange.bind(this);
}
componentDidMount() {
// This is reducer function to get my data from server. I'm getting data and able to display it to my element.
// I'm using redux for my data layer.
this.props.getuserDetails(userId);
}
I'm not sure how to update the input element value using state as well as the value i received from props via reducer function.
handleInputChange (event) {}
this.setState({
firstName: event.target.value
});
}
inside my render function.
Textfield displays the user firstname. But its not allowing me to edit the field. Because i have bind the props.reducer directly.
<TextField name="firstName" onChange={this.handleInputChange} floatingLabelText="First Name" value={this.props.reducer.result.firstName}/>
How do i bind the data from dispatcher/reducer. and also accept user input to edit the first name.
later i can update it in server
I'm struck with this issue. any help is appreciated. Thanks.
After componentDidMount, this.props.getuserDetails(userId) function will set the props again, you should set the state in componentWillReceiveProps. and use this.state.firstname instead of this.props.reducer.result.firstname
code:
componentWillReceiveProps(nextProps){
if(this.state.firstName != nextProps.reducer.result.firstname)
{
this.setState({
firstName: nextProps.reducer.result.firstname
});
}
}
this will update the props to state and use this.state.firstname
<TextField name="firstName" onChange={this.handleInputChange} floatingLabelText="First Name" value={this.state.firstName}/>
You're right. You can't update your input element using setState() since it's value propagated through and bound to props.
Depending on what you're trying to achieve, there are different solutions available..
Update props by dispatching an action each time your input changes: e.g.:
handleInputChange (e) {}
// NOTE: I just made up this action
this.props.setUserDetails(e.target.value);
}
Set initialState from props and bind the input to state (this is considered an anti pattern, though it's OK if you make clear that it's only used for initial state. Also, react will not update the value of the input field after it has been rendered):
componentDidMount() {
this.setState({ value: this.props.value })
}
handleInputChange (e) {}
this.props.setState({ value: e.target.value });
}
Merge props and state whenever props change.
...
But anyway, as said before, it depends on what you're trying to do.
Here's a more detailed answer
I have the following component in a Redux app for recipes, which currently only has a name right now, for simplicity sake.
class RecipeEditor extends Component {
onSubmit = (e) => {
e.preventDefault()
this.props.updateRecipe(this.props.recipe, { name: this.refs._name.value })
}
render = () => {
if (!this.props.recipe) {
return <div />
}
return (
<div>
<form onSubmit={this.onSubmit}>
<label>Name: </label>
<input type="text" ref="_name" value={this.props.recipe.name} />
<input type="submit" value="save" />
</form>
</div>)
}
static propTypes = {
recipe: React.PropTypes.shape({
name: React.PropTypes.string.isRequired
})
}
}
This gives me an editor with a textbox that can't be edited. There's a warning in the console as well:
Warning: Failed form propType: You provided a value prop to a form
field without an onChange handler. This will render a read-only
field. If the field should be mutable use defaultValue. Otherwise,
set either onChange or readOnly. Check the render method of
RecipeEditor.
That makes sense, but I don't want an onChange event, I'll use ref to get the values on submit. It's not a readonly field obviously, so I try changing it to have a defaultValue.
<input type="text" ref="_name" defaultValue={this.props.recipe.name} />
This gets closer to the behavior I'm looking for, but now this only sets the recipe when the control is mounted and it no longer updates when a new recipe is chosen.
Is the solution having a handler on every input field that sets state, and then in submit, take all the state and update the recipe?
When you use an input element with the valueattribute set, it becomes a "controlled" component. See this page for a more detailed explanation:
https://facebook.github.io/react/docs/forms.html#controlled-components)
Long story short, that means that on every render you are setting the value attribute to the value from the props, which will stay the same unless you also update the value in your redux store).
When the input is "uncontrolled" instead (value attribute not explicitly set), its internal state (the value string) is handled implicitly by the browser.
If for some reason you prefer to keep the state locally and you don't want to dispatch a redux action every time the value changes with onChange, you can still manage the state yourself using React component state and dispatch the action on submit:
class RecipeEditor extends Component {
state = {
recipeName: ''
}
onSubmit = (e) => {
e.preventDefault()
this.props.updateRecipe(this.props.recipe, { name: this.state.recipeName })
}
handleNameChange = (e) => {
this.setState({ recipeName: e.target.value })
}
render = () => {
if (!this.props.recipe) {
return <div />
}
return (
<div>
<form onSubmit={this.onSubmit}>
<label>Name: </label>
<input type="text" ref="_name" value={this.state.recipeName} onChange={this.handleNameChange} />
<input type="submit" value="save" />
</form>
</div>)
}
static propTypes = {
recipe: React.PropTypes.shape({
name: React.PropTypes.string.isRequired
})
}
}
In this example, whenever the input value changes you store the current value in the state. Calling setState triggers a new render, and in this case it will set the value to the updated one.
Finally, note you don't have to use onChange if you never need to set the input value to something specific. In this case, you can remove the value attribute and just use refs. That means though that if somewhere else in your code you change the value stored in Redux state, you won't see the change reflected in your input. As you've seen, you still can set the initial value to something specific using intitalValue, it just won't be in sync with your redux store.
However this is not going to work well if for example you want to reuse the same form to edit an existing recipe you have in your store.
As React already mentioned in the console, you have to provide an "onChange" listener to make that field editable.
The reason behind that is the value property of the input field which you provided
{this.props.recipe.name}
This value doesn't changes when you type in the input field.
And since this doesn't changes, you don't see the new value in your input field. (Which makes you feel that it is non editable.)
Another thing to note here is that the "recipe.name" should be transferred to the state of your component so that you can set a new state inside "onChange" listener.
see the usage of "setState" function.