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
Related
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.
There are plenty of examples of changing context directly - such as this:
export class UserProvider extends React.Component {
updateUsername = newUsername => {
this.setState({ username: newUsername });
};
state = {
username: "user",
updateUsername: this.updateUsername
};
}
followed by this in the Consumer:
<input
id="username"
type="text"
onChange={event => {
updateUsername(event.target.value);
}}
/>
but my onChange handler is a function and within that OnChange handler, I want to update a context variable.
Using this example, how would I call updateUsername from within the OnChange Handler or indeed from any function such as componentDidMoount, etc
I have searched but so far not been able to find anything that might give an indication as to how to accomplish this.
Using Context in React you'd usually want to have reducers when updating state, reducers allow you to dispatch actions that mutate state repetitively. Don't update state directly it's regarded as anti-pattern.
I wanted to add this a comment however my reputation doesn't allow me to do so yet, excuse me if this doesn't solve your problem.
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....
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.