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.
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
So we all know uncontrolled components are usually a bad thing, which is why we usually want to manage the state of an input (or group of inputs) at a higher-level component, usually some kind of container. For example, a <Form /> component manages state and passes down state as values to its <Input /> components. It also passes down functions such as handleChange() that allow the Input to update the state.
But while implementing my own <NumericInput /> component, it got me thinking that fundamentally this component is not self-reliant. It's reusable but requires a lot of repetition (opposite of DRY mentality) because everywhere in my app that I want to use this component, I have to implement these state values, a handleChange function, and in the case of my <NumericInput />, two additional functions to control the stepper arrows.
If I (or someone who took over my code) wanted to use this <NumericInput />, but they forget to run to a different container component and copy the stepUp() and stepDown() functions to pass down as props, then there will just be two non-functional arrows. I understand that this model allows our components to be flexible, but they also seem to be more error-prone and dependent on other components elsewhere. Again, it's also repetitive. Am I thinking about this incorrectly, or is there a better way of managing this?
I recognize this is more of a theory/design question, but I'm including my code below for reference:
NumericInput:
const NumericInput = ({label, stepUp, stepDown, ...props}) => (
<>
{label && <Label>{label}</Label>}
<InputContainer>
<Input type={props.type || "number"} {...props} />
<StepUp onClick={stepUp}>
//icon will go here
</StepUp>
<StepDown onClick={stepDown}>
//icon will go here
</StepDown>
</InputContainer>
</>
);
Form.js
const Form = (props) => {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
}
const stepUp = () => {
setValue(value++);
}
const stepDown = () => {
setValue(value--);
}
return (
<NumericInput
stepUp={stepUp}
stepDown={stepDown}
handleChange={handleChange}
label="Numeric"
)
}
Let's try to boil down your questions a bit:
[NumericInput] is reusable but requires a lot of repetition (opposite of DRY mentality) because everywhere in my app that I want to use this component, I have to implement these state values, a handleChange function, and in the case of my , two additional functions to control the stepper arrows.
The only repetition you have is to define a value and a handleChange callback property for <NumericInput />. The value prop contains your numeric input value and is passed in from the parent container component owning that state. You need the callback to trigger a change request from the child (controlled component). That is the ususal approach, when you need to lift state up or divide your components in container and presentational components.
stepUp/stepDown methods are an implementation detail of NumericInput, so you can remove them from Form. Form just wants to know, if a value change has been triggered and what the new changed numeric value is.
Concerning where to store the state: There is probably a reason, why you want to save the state in the Form component. The user enters some input in (possibly) multiple fields before pressing the submit button. In this moment your form needs all the state of its child fields to trigger further processing based on the given input. And the idiomatic React way is to lift state up to the form to have the information.
I understand that this model allows our components to be flexible, but they also seem to be more error-prone and dependent on other components elsewhere. Again, it's also repetitive.
A presentational component is rather less error prone and is independent, because it doesn't care about state! For sure, you write a bit more boilerplate code by exposing callbacks and value props in child. The big advantage is, that it makes the stateless components far more predictable and testable and you can shift focus on complex components with state, e.g. when debugging.
In order to ease up props passing from Form to multiple child components, you also could consolidate all event handlers to a single one and pass state for all inputs. Here is a simple example, how you could handle the components:
NumericInput.js:
const NumericInput = ({ label, value, onValueChanged }) => {
const handleStepUp = () => {
onValueChanged(value + 1);
};
const handleStepDown = () => {
onValueChanged(value - 1);
};
return (
<>
...
<Input value={this.state.value} />
<StepUp onClick={handleStepUp}></StepUp>
<StepDown onClick={handleStepDown}></StepDown>
</>
);
};
Form.js:
const Form = (props) => {
const [value, setValue] = useState(0);
const handleValueChanged = (value) => {
setValue(value);
}
return (
<NumericInput
onValueChanged={handleValueChanged}
value={value}
label="Numeric"
)
}
Move the setValue function to the NumericInput component
Manage your state through the component.
return (
<NumericInput
setValue={setValue}
label="Numeric"
/>
)
I would recommend you use hooks
I currently use
<TextField onChange={e => this.change(e, items)}
This gets fired at every single letter I put into the TextField, consequently, text I type gets filled in slow motion. I was thinking it'd be better if this request goes out once the user types everything and focuses away. What kind of event I can use in this scenario with React & Material UI TextField ?
Having a debounced version of your function is useful when any DOM event is attached. It reduces the number of calls to this function to the bare minimum and thereby improve the performance.
So, you can do this:
import _ from 'lodash';
constructor(props) {
super(props)
this.onChangeDebounce = _.debounce(e => this.change(e, items), 300);
}
render() {
...
onChange={e => this.onChangeDebounce(e)}
...
}
In this case, I am passing to debounce only two parameters:
The function to be rate-limited
The rate limit in milliseconds, ie., the time to wait before the function is fired.
Or you can use onBlur event, that is available for any DOM element. The onBlur event happens whenever an input loses focus on. In other words: when you remove your cursor from within the input, it loses "focus", or becomes "blurry".
The caveat is that it doesn't have an associated event, so, to reach what you want, you can update the state with the field value and on onBlur, retrieve this value from state.
Here you have a fiddle doing this.
After one and a half year, here is a contemporary approach mostly for functional components:
Keep the value of the field as a React state.
Set it onChange
Persist (or do whatever expensive process) onBlur
So, a component containing this text field would, in a way, look like similar to this:
import React, { useState } from 'react'
import { TextField } from '#material-ui/core'
const MyFunctionalComponent = () => {
const [textFieldValue, setTextFieldValue] = useState('')
// ...
return (
<React.Fragment>
{/** ... */}
<TextField
value={textFieldValue}
onChange={(e) => setTextFieldValue(e.target.value)}
onBlur={() => {
console.log(`I am blurred and ready to process ${textFieldValue}`)
}}
/>
{/** ... */}
</React.Fragment>
)
}
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.
The following is a custom dropdown menu that I would like to make compatible with Redux Form, as well as execute custom onChange function (e.g.: dispatch an action)
<Select
label='Team'
name='team'
options={teamOptions}
placeholder='Team'
onClick={this.handleTeamClick}
/>
In a first step, I created the wrapper as follows:
export const rfSelect = ({ input, options, meta, ...rest }) => (
<Select {...input} onChange={(e,v) => input.onChange(v.value)} options={options} {...rest} />
)
where the default onChange() of the Select component is called.
The wrapper component is used with Redux Form like this:
<Field
component={rfSelect}
label='Team'
name='team'
options={teamOptions}
placeholder='Team'
onClick={this.handleTeamClick}
/>
So far, everything is good. The UI element displays the new selected value when a new value is chosen from the dropdown.
In step 2, I would like to pass a custom onChange event. This is what it looks like:
<Field
component={rfSelect}
label='Team'
name='team'
options={teamOptions}
placeholder='Team'
onClick={this.handleTeamClick}
onChange={this.handleTeamChange}
/>
handleTeamChange(e, sel) {
this.props.dispatch(bla.handleTeamChange(sel.value))
}
export const rfSelect = ({ input, options, meta, ...rest }) => (
<Select {...input} onChange={(e,v) => input.onChange(v.value)} options={options} {...rest} />
)
Here, the default onChange was replaced by the custom one that I passed in.
handleTeamChange is called correctly and the action is dispatched.
However, passing my own onChange now no longer updates the selected value upon choosing from the dropdown. In other words, i select a dropdown item but the value remains blank.
Bottomline: I am either able to have the UI component update the selected value correctly, or execute my custom onChange event. But not both.
Here are the ideas that I could think of in order to solve this problem:
in the custom onChange function, handle the update of the selected value manually
pass in the custom onChange function as onChange2 instead of onChange (meaning it will not overload the default function), and then let the component wrapper call both input.onChange(...) and onChange2(...).
Though both these alternatives are quite hacky, and I am not particularly fond of either. I have not yet been able to code them successfully either, so they are just ideas so far.
For the first option, printing e in the handleTeamChange function above shows the following, which i have tried (and failed) to use to manually set the Select component's value:
Any input on this would be greatly appreciated.
Thanks!
Allowing custom onChange handlers to be given to Field is a feature I'm considering for a future version of the library.
You are on the right track with your hack ideas. Something like this should work:
export const rfSelect = ({ input, meta, onChange, ...rest }) => (
<Select {...input} onChange={(e,v) => {
input.onChange(v.value) // update redux-form value
onChange(v.value) // call your additional listener
}} {...rest} />
)
I removed options, because it's there in ...rest, and you can even call your prop onChange as it's separate from the one in input.
Hope this helps.