Some Thoughts On Forms in React

A sane approach to Forms in React.

A. Sharif
A. Sharif
Jul 6, 2017 · 11 min read

View Driven

Let’s call the first approach React/JSX Driven, which is meant as tackling the problem via form elements. Building abstractions over elements like form or input for example.

<Input 
onChange={doSomething}
label='Name Field'
errorMessage={getSomeMessage()}
{/* */}
/>
<Form onSubmit={doSomething} errors={getFormErrors()} >
<Input onChange={doSomethingElse} />
{/* ... */}
</Form>

Model Driven

On the other side of the spectrum, there is a model driven approach, which creates elements from a given set of data. In the most extreme form having knowledge of the data types, creating validators and specific inputs based on the field’s type. Again abstracting away the manual part of having to write the initial form and its corresponding elements and their attributes.

const schema = { 
name: type.string,
customerId: optional(type.number)
}
// ...<SpecialForm structure={schema} {/* ... */}/>
{/* maybe add own elements too */}
</SpecialForm>

A Mix of Model and View Driven

Another approach is to tackle the problem from both, the model and the view side. Using the model for the validation and abstracting over the regular form elements interconnected via ContextType i.e.

Redux / State Management Driven

Then of course there is always the possibility to use existing state management solutions like Redux and others to handle the form state. In the most simplest case using react-redux connect to add state management capabilities to a container containing the form.


Back to square one: Some Background on Forms in React.

Before we continue, let’s take a step back and see what surprises most people when they start working with forms in React. A common question is: How do we validate and update our form fields? To answer that question, we need to refer back to the Uncontrolled Forms and Controlled Forms sections in the React documentation. So, one can either access the form state via refs, in case of uncontrolled forms or use the value attribute and set the field values manually. With the latter being the recommended solution.

type Data = {
firstName: string,
lastName: string,
userName: string,
confirmUserName: string,
notifications: boolean,
}
// render function ...
<form onSubmit={handleSubmit}>
<label>
First Name:
<input
type="text"
name="firstName"
value=
{state.firstName}
onChange={handleChange}
/>
</label>
<label>
Last Name:
<input
type="text"
name="lastName"
value=
{state.lastName}
onChange={handleChange}
/>
</label>
<label>
User Name:
<input
type="text"
name="userName"
value=
{state.userName}
onChange={handleChange}
/>
</label>
<label>
Confirm User Name:
<input
type="text"
name="confirmUserName"
value=
{state.confirmUserName}
onChange={handleChange}
/>
</label>
<label>
Notifications:
<input
name="notifications"
type="checkbox"
checked=
{state.notifications}
onChange={handleChange} />
</label>
<br />
<label>
<input type="submit" value="Submit" />
</form>
handleChange(event) {
const target = event.target;

this.setState({
[ target.name]: target.type === 'checkbox'
? target.checked
: target.value
});
}
constructor(props) {
super(props)
this.state = {
firstName: '',
lastName: '',
userName: '',
confirmUserName: '',
notifications: false
}
}
<Form initialState={initialState} />

Back to the Efficiency Topic

As we have seen, creating the form structure is not the problem here, it even leaves us more room on deciding how these inputs can be styled, so we can see that the initial JSX or view code we need to write is maybe repetitive but nothing worth abstracting away and sacrificing flexibility for. Now we are free to choose a UI library to render our inputs or we can move these labels or error message anywhere we want to. This is not the real problem.

Validation

Now if you recall, our form has no validation capabilities yet. So let’s see how we can approach the validation topic in a cautious manner, just because as opposed to state management, which can be solved with a single function, we need to think about how validation needs to happen from a user perspective. Does validation occur onChange or onBlur or onKeyUp? Or will the form values be validated when submitting the form?

const errors = {}if (this.state.street.length <= 3) {
error.street = 'Street has min length of 4'
}
import {
compose,
curry,
path,
prop,
} from 'ramda'


// validations
const isNotEmpty = a => a.trim().length > 0
const hasCapitalLetter = a => /[A-Z]/.test(a)const isGreaterThan = curry((len, a) => (a > len))const isLengthGreaterThan = len =>
compose(isGreaterThan(len), prop('length'))
const validationRules = {
name: [
[ isGreaterThan(5),
`Minimum Name length of 6 is required.`
],
],
random: [
[ isGreaterThan(7), 'Minimum Random length of 8 is required.' ],
[ hasCapitalLetter,
'Random should contain at least one uppercase letter.'
],
]
}
const spec = {
street: [[isLengthGreaterThan(3), 'Street has min length of 4']]
}
spected(spec, {street: 'foo'})

Connecting the Dots

So once we have a solution for validating the input independent from the form itself, we will want to connect the validation with the form.

const createForm = ({ 
// define functions like onChange, validate etc.
}) => {
// define and return class
}
const createForm = ({
mapSetStateToProps = (updateState, actions) => ({
onChange: e => {
const { name, value } = getValueName(e)
return updateState(actions.update(name, value))
},
}),
actions = {
update: (name, value, state) => {
return [assocPath(['values', name], value, state)]
},
}
}) => Component => {}
const createForm = ({
mapSetStateToProps,
actions,
}) => Component => {
return class HigherOrderFormComponent extends React.Component {
constructor(props) {
super(props)
this.state = { values:props.values }
this.actions = R.map(
f => (...args) => f(...args, this.state),
actions
)
}

updateState = (setState) => {
const [setStateFn, cb = () => {}] = setState
this.setState(setStateFn, () => cb(this.state))
}

render() {
const dispatchers =
mapSetStateToProps(this.updateState, this.actions)
return React.createElement(Component, {
...this.props,
...dispatchers,
state: this.state.values,
})
}
}
}
const enhanceForm = createForm({})
const EnhancedForm = enhanceForm(Form)

<EnhancedForm values={values} />

Validation

Once we have validation rules defined, we can run these against the actual form state and keep track of any errors via local state, which we can pass down to the wrapped component again. But we know for a fact that the validation itself can be detached from the actual field value update, i.e. validating oBlur. Let’s see how this would work by writing some actual code.

validateFns = {
all: (data) =>
spected(basicValidationRules, data),
input: (name, value) =>
spected(
pick([name], basicValidationRules), {[name]: value}
)
}
createForm({ validate: validateFns })
const createForm = ({
validate,
mapSetStateToProps = (updateState, actions) => ({
// ...
validate: e => {
const { name, value } = getValueName(e)
return updateState(actions.validate(name, value))
},
// ...
}),
actions: {
validate: (name, value, state, {validate}) => {
return [
R.assoc('errors', validate.input(name, value), state)
]
},
}
}
{ firstName: ['First Name is required'] } // in case of an error
{ firstName: [] } // in case of success
validateAll: (cbFn, state, { validate }) => {
return [
assoc('errors', validate.all(state.values), state),
(state) => {
if (isValid(state.errors)) {
cbFn(state.values)
}
}
]
}
mapSetStateToProps = (updateState, actions) => ({
// ...
onSubmit: (onsSubmitFn) => {
return updateState(actions.validateAll(onsSubmitFn))
}
}),

What’s Next?

In part 2 we will focus on asynchronous actions, how to debounce and how to react to the current form state, i.e. switching to dynamic inline validation as soon as the form has been submitted for the very first time etc.


JavaScript Inside

All things JavaScript.

A. Sharif

Written by

A. Sharif

Focusing on quality. Software Development. Product Management.

JavaScript Inside

All things JavaScript.