I am experimenting with react right now and I am trying to understand how batching work. As I understand it, when I use function in setState, it reads the current state reliably. I defined an input tag and I can update the Input text when it accepts object as the following:
const [inputText, setInputText] = useState('');
<input
onChange={(e) => {
setInputText(e.target.value);
}}
/>
However, when I tried to update state with function as the following:
<input
onChange={(e) => {
setInputText((inputText) => e.target.value);
}}
/>
for second onchange event trigger, it throws "Cannot read property 'value' of null". Why is that?
Events are pooled. And the setState callback is async. So when you extract the value the event is already back in pool.
<input
onChange={(e) => {
const {value} = e.target;
setInputText((inputText) => value);
}}
/>
BTW you don't really need the callback version of set state in this example because next value does not depend on the previous one.
Related
Let us assume that I have the following state:
const [state, setState] = useState<CustomType>({
foo: '',
bar: ''
});
I also have two different Input fields in my component:
<Input value={state.foo} onChange={e => someFunc(params)} />
<Input value={state.foo} onChange={e => someFunc(params)} />
I am aware that I must update states via their setter received from useState, but I would like to implement a generic someFunc() method that would receive an object as parameter and updates its property with its setter. Enforcing strict type usage, providing intellisense for development and type checking are my priorities.
Right now I have managed to write a generic method for this goal:
function updateObject<Type>(
event: ChangeEvent<HTMLInputElement>,
object: Type,
propertyName: keyof Type
): Type {
return {
...object,
[propertyName]: event.target.value
};
}
This could be called like:
<Input value={state.foo} onChange={e => setState(someFunc(e, state, 'foo'))} />
<Input value={state.foo} onChange={e => setState(someFunc(e, state, 'bar'))} />
I think this onChange introduces unnecessary complexity. I could refactor to receive the setter function as parameter in someFunc() method instead of passing its returning value to the setter in order to increase readability but I am wondering if there are other solutions? E.g. can I refer to the returning value of useState() somehow to include both the state and its setter? So I could call it like using something like onChange={e => someFunc(e, magicalReferenceIDontKnowOf, propertyNameToBeChangedInReferencedStateObject)}.
Background: I wish to have this someFunc() available for all components in my React application so
instead of having to write different useStates for all variables I can collect logically related data in typed objects and handle them with a generic method
instead of defining updating logic individually for each onChange
event I can call this generic method receiving an event,
object-with-setter and property-to-be-modified as parameters
I have a very simple setup. I just have one controlled input and I want to console.log the input.
import React, {useState} from 'react'
const App = () => {
const [text, setText] = useState('')
const handleChange = event => {
event.preventDefault()
setText(_prev => event.target.value)
consoel.log(text)
}
return(
<div>
<input type="text" value={text} onChange={handleChange} />
</div>
)
}
I seem to only be getting the one before the actual state. For example if I type 'abc' in the console i only see 'ab' and after typing a fourth character I see 'abc'. Why is my state always one input behind?
(trying to change the state in the following manner setText(event.target.value) has provided the same results).
Due to it's asynchronous behavior you can't directly get value right after it's being updated. To get the value, you can use the effect hook which will check for the change and acts upon the behavior:
useEffect(() => {
console.log(text) // act upon
}, [text]) // when text changes, it runs
Also, side note, you just have to do:
setText(event.target.value)
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.
recompose has this function called withHandlers that lets you define an event handler while keeping your component pure.
e.g. if you had something like this inside you render() method:
<TextInput value={value} onChange={ev => this.handleChange(ev.target.value)} />
it wouldn't be pure because every time your component renders, it'd by passing a different onChange function to your TextInput component.
This is great, but how can this be extended to support arrays of inputs? Or otherwise provide auxiliary data?
e.g. taking their example and extending it a bit:
const enhance = compose(
withState('values', 'updateValue', ''),
withHandlers({
onChange: props => inputName => event => {
props.updateValue(inputName, event.target.value)
},
onSubmit: props => event => {
event.preventDefault();
submitForm(props.value)
}
})
)
const Form = enhance(({ value, onChange, onSubmit, inputs }) =>
<form onSubmit={onSubmit}>
<label>Value
{inputs.map(input => (
<TextInput value={value} onChange={onChange(input)} />
))}
</label>
</form>
)
I've fudged the details a bit, but pretend inputs comes in as an array of input names. e.g. ["firstName","lastName"] would render two textboxes, one for each.
I want to store the values for each of these in my state, but I don't want to define separate updater functions for each. Thus I need to attach some metadata to the onChange={...} prop so that I know which field I'm updating in my state.
How can I do that?
In my example I wrote onChange(input) and added an extra 'level' to the withHandlers.onChange function to accept the extra argument, but withHandlers doesn't actually work that way. Is there some way to do this -- i.e., ensure that each TextInput receives the same function instance every time <Form> is rendered?
That's a typical case where you need to define the change handle directly inside your TextInput component.
You'd need to pass updateValue function as a prop to TextInput components.
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.