I have a number of these components, but will show just two as an example. They're very similar, but the differences are enough to where splitting them out felt needed. What I really need is some help. Is this the right approach, or would I be better served somehow combining them to reduce duplicate code? I have similar components for toggles like radios and checkboxes, as well as textarea that handle both plain text and markdown fields, etc.
Other feedback would also be appreciated :)
Textfield wrapper.
'use strict'
import React, { Component } from 'react'
import { FieldText, FieldLabel, FieldDescription, FieldErrorMessage } from './'
import PropTypes from 'prop-types'
class FieldTextWrap extends Component {
static propTypes = {}
constructor(props) {
super(props)
this.state = {
value: '',
isValid: true
}
}
_handleChange = value => {
const { formatter } = this.props
const formattedValue = !!formatter ? formatter(value) : value
this.setState({ value: formattedValue })
}
componentDidMount() {
const {
required,
type,
value,
initialValue,
dispatch,
handleBlur,
isForm,
formId,
id
} = this.props
!!initialValue && this.setState({ value: initialValue })
!initialValue && !!value && this.setState({ value })
const fieldData = {
id,
value,
type,
required,
formId,
valid: true
}
return isForm && dispatch(handleBlur(null, dispatch, fieldData))
}
componentDidUpdate(prevProps, prevState) {
const { validationRule, required } = this.props
const { value } = this.state
const didUpdate = prevState.value !== value
// validate and update
const isValid = !!validationRule
? validationRule.func({
field: { required, value },
tests: validationRule.tests,
options: validationRule.options
})
: true
didUpdate && this.setState({ isValid })
}
componentWillUnmount() {
const { removeFormField, id, isForm } = this.props
isForm && removeFormField(id)
}
render() {
const {
className = '',
description,
fieldGroup = '',
formName,
formId,
hideDescription,
hideLabel,
id,
label,
required,
requireDefault,
type,
validationText: message = ''
} = this.props
const classes = `form-item ${formName}__${id} ${fieldGroup} ${type}-field ${className} ${type} text-type`
const value = this.state.value
const isValid = this.state.isValid
const fieldData = {
id,
value,
type,
required,
formId,
valid: isValid,
message
}
return (
<div className={classes}>
<FieldLabel
name={id}
label={label}
required={required}
requireDefault={requireDefault}
hidden={hideLabel}
/>
<div className={`${type}-field__wrapper`}>
{isValid ? (
<FieldDescription
name={id}
description={description}
hidden={hideDescription}
/>
) : (
<FieldErrorMessage name={id} message={message} />
)}
<FieldText
{...this.props}
value={value}
handleChange={this._handleChange}
fieldData={fieldData}
/>
{type === 'date' && <div className="date-field__icon" />}
</div>
</div>
)
}
}
export default FieldTextWrap
Select Wrapper
'use strict'
import React, { Component } from 'react'
import {
FieldSelect,
FieldLabel,
FieldDescription,
FieldErrorMessage
} from './'
import PropTypes from 'prop-types'
class FieldSelectWrap extends Component {
static propTypes = {}
constructor(props) {
super(props)
this.state = {
value: '',
isValid: true
}
}
_handleChange = value => this.setState({ value })
componentDidMount() {
const {
required,
type,
value,
initialValue,
dispatch,
handleBlur,
isForm,
formId,
id
} = this.props
!!initialValue && this.setState({ value: initialValue })
!initialValue && !!value && this.setState({ value })
const fieldData = {
id,
value,
type,
required,
formId,
valid: true
}
return isForm && dispatch(handleBlur(null, dispatch, fieldData))
}
componentDidUpdate(prevProps, prevState) {
const { validationRule, required } = this.props
const { value } = this.state
const didUpdate = prevState.value !== value
// validate and update
const isValid = !!validationRule
? validationRule.func({
field: { required, value },
tests: validationRule.tests,
options: validationRule.options
})
: true
didUpdate && this.setState({ isValid })
}
componentWillUnmount() {
const { removeFormField, id, isForm } = this.props
isForm && removeFormField(id)
}
render() {
const {
className = '',
description,
fieldGroup = '',
formName,
formId,
hideDescription,
hideLabel,
id,
label,
required,
requireDefault,
type,
validationText: message = ''
} = this.props
const classes = `form-item ${formName}__${id} ${fieldGroup} ${type}-field ${className} ${type}`
const value = this.state.value
const isValid = this.state.isValid
const fieldData = {
id,
value,
type,
required,
formId,
valid: isValid,
message
}
return (
<div className={classes}>
<FieldLabel
name={id}
label={label}
required={required}
requireDefault={requireDefault}
hidden={hideLabel}
/>
<div className={`${type}-field__wrapper`}>
<FieldDescription
name={id}
description={description}
hidden={hideDescription}
/>
{!isValid && <FieldErrorMessage name={id} message={message} />}
<FieldSelect
{...this.props}
value={value}
handleChange={this._handleChange}
fieldData={fieldData}
/>
{type === 'date' && <div className="date-field__icon" />}
</div>
</div>
)
}
}
export default FieldSelectWrap
-
\$\begingroup\$ How would they be used? Is their usage similar as well? \$\endgroup\$Mast– Mast ♦2018年08月13日 15:33:55 +00:00Commented Aug 13, 2018 at 15:33
-
\$\begingroup\$ I'm using these to build forms. The first is the primary component called when including text fields, and the second when including select fields. So, their usage is similar, being children of my form module, but different in that they're used to manage different types of fields. Hopefully, that makes sense. \$\endgroup\$FranCarstens– FranCarstens2018年08月13日 15:48:12 +00:00Commented Aug 13, 2018 at 15:48
1 Answer 1
Combining them to reduce duplicate code is not a good approach. Prefer small composable components over large Swiss army knife components. Think single responsibility principle.
If your component takes a lot of properties, it may be a good candidate to make smaller. If you are testing for type, you may want to create multiple controls.
{type === 'date' && <div className="date-field__icon" />}
In your example, I recommend generating a generic wrapper component with a nested child.
Create wrapper component:
class LabelWrapper extends Component {
...
render() {
const {text, icon, error, children} = this.props;
const ErrorMessage = this.getError(error);
const Icon = this.getIcon(icon);
return (
<div className="label-wrapper">
<label>{text}</label>
<ErrorMessage/>
{children}
<Icon>
</div>
);
}
getIcon(icon) {
return icon && <div className={icon} />}
}
}
Use Wrapper in form passing in child component:
<LabelWrapper
text='Full Name'
error={error.fullName}>
<TextInput
id='fullName'
value={form.firstName}
handleChange={handleChange.firstName}/>
</LabelWrapper>
<LabelWrapper
text='Choose Date'
icon='date-field__icon'
error={error.date}>
<FieldSelect
value={form.date}
handleChange={handleChange.chooseDate}
fieldData={fieldData} />
</LabelWrapper>
You can also compose a more specific control. This will prevent your form from getting too large.
class ChooseDate extends Component {
...
render() {
const {fieldData, handleChange, value, error} = this.props;
return (
<LabelWrapper
text='Choose Date'
icon='date-field__icon"'
error={error}
<FieldSelect
value={value}
handleChange={handleChange}
fieldData={fieldData}/>
</LabelWrapper>
);
}
}
A few other notes:
- Duplicate react code is fine in many situations (15:20)
- Keep logic out of the render
- Move show / hide logic into helper functions
- Move complex logic into selectors to keep the control simple
Resources: