Better form state management for React where data state is directly mapped to form fields, so form becomes just a representation and changing interface for that data state.
Let's say you have some data and you want to represent it as an HTML form with an Input for each data field.
"user": {
"name": "Pepe",
"status": "sad",
"friends": [
"darkness"
]
}
Each data field can be referenced with a "key" or "property" path. You might be familiar with this concept from working with immutable data structures or helpers like lodash.get()
.
"user": {
"name": "Pepe", // "user.name"
"status": "sad", // "user.status"
"friends": [
"darkness" // "user.friends.0"
]
}
The first core idea of NeoForm is to map data to form fields using these key/property paths. We'll refer to this data as "form state" below.
Let's see how it works with a step-by-step example. First, we need to install the following set of dependencies:
yarn add prop-types recompose neoform neoform-validation neoform-plain-object-helpers
We'll start with creating a simple input:
const MyInput = () => (
<input/>
);
export default MyInput;
After wrapping this input with field
HOC from NeoForm we'll have:
A value
from a form state (can be used in checkbox as a checked
attribute if it's boolean, and so on) and onChange
handler to let NeoForm know that value should be changed:
import { field } from 'neoform';
const MyInput = ({ value, onChange }) => (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
export default field(MyInput);
Use (e) => e.target.checked
if you have a checkbox or just (value) => value
if you have some custom/3rd-party field implementation.
Now when the input is ready we can use it in a form:
import MyInput from '../MyInput';
const MyForm = () => (
<form>
<MyInput name="user.name"/>
<MyInput name="user.status"/>
<MyInput name="user.friends.0"/>
</form>
);
export default MyForm;
Let's connect this form to NeoForm by wrapping it with a form
HOC:
import { form } from 'neoform';
import MyInput from '../MyInput';
const MyForm = () => (
<form>
<MyInput name="user.name"/>
<MyInput name="user.status"/>
<MyInput name="user.friends.0"/>
</form>
);
export default form(MyForm);
Finally, we assemble everything together:
import { setValue, getValue } from 'neoform-plain-object-helpers';
import MyForm from '../MyForm';
class App extends Component {
constructor(props) {
super(props);
this.state = {
data: props.data
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
onChange(name, value) {
this.setState((prevState) => setValue(prevState, name, value));
}
onSubmit() {
console.log('submit:', this.state.data);
}
render() {
<MyForm
data={this.state.data}
getValue={getValue}
onChange={this.onChange}
onSubmit={this.onSubmit}
/>
}
}
What's going on here? As you may guessed, all fields in NeoForm are controlled. So, in order to update them, we need to update data state:
First, we need to specify getValue
prop to tell NeoForm how exactly it should retrieve field value from data state. The reason to do that is because you might have a plain object data, Immutable or something else with a different "interface".
Instead of writing your own getValue
function, you can use one from neoform-plain-object-helpers or neoform-immutable-helpers package.
getValue
arguments:
data
— form data statename
— field name
Second, we have only one onChange
handler for the entire form instead of multiple ones for each field. So, whenever some field requests a change, we need to update form data by updating the state so updated value is passed to that field with a new render.
ℹ️ Consider using Recompose pure()
HOC or React.PureComponent
for fields to avoid unnecessary renders and get performance boost in some cases.
Instead of writing your own handler, you can use setValue
helper from neoform-plain-object-helpers or neoform-immutable-helpers package.
setValue
arguments:
data
— form data statename
— field namevalue
— new field value
+--------------+
| |
| |
| +---------v---------+
| | |
| | MyForm.data |
| | |
| +---------+---------+
| |
| name |
| |
| +---------v---------+
| | |
| | MyInput.value |
| | |
| +---------+---------+
| |
| |
| +---------v---------+
| | |
| | MyInput.onChange |
| | |
| +---------+---------+
| |
| name | value
| |
| +---------v---------+
| | |
| | MyForm.onChange |
| | |
| +---------+---------+
| |
| name | value
| |
+--------------+
Validation in NeoForm is always asynchronous.
fieldValidation
is another HOC:
import { field } from 'neoform';
import { fieldValidation } from 'neoform-validation';
const MyInput = ({
validate,
validationStatus,
validationMessage,
...props
}) => (
<input {...props} onBlur={validate} />
{
validationStatus === false && (
<span>{validationMessage}</span>
)
}
)
export default field(fieldValidation(MyInput));
Where the props are:
validate
– validation action, can be called whenever you want (onChange
,onBlur
, etc)validationStatus
–true
|false
|undefined
status of field validationvalidationMessage
– an optional message passed from validator
import { form } from 'neoform';
import { formValidation } from 'neoform-validation';
import MyInput from '../MyInput';
const MyForm = ({
/* data, */
validate,
validationStatus,
onInvalid,
onSubmit
}) => (
<form onSubmit={(e) => {
validate(onSubmit, onInvalid)
e.preventDefault();
}}>
<MyInput name="user.name"/>
<MyInput name="user.status"/>
<MyInput name="user.friends.0"/>
</form>
);
export default form(formValidation(MyForm));
Where:
validate
– entire form validation action: it will validate all fields and if they're valid it will invoke a first provided callback (onSubmit
handler in most cases) or second callback (something likeonInvalid
) if they're invalidvalidationStatus
–true
|false
|undefined
status of entire form validation
"Validator" is just a Promise. Rejected one is for validationStatus: false
prop and resolved is for validationStatus: true
. An optional argument passed to a rejected or fulfilled Promise becomes validationMessage
prop.
export const requiredValidator = (value, type) => {
if (value === '') {
return Promise.reject('💩');
}
return Promise.resolve('🎉');
};
Where:
value
– field value for validationtype
– event type. Can besubmit
,change
,blur
or anything you will provide to fieldvalidate
method
It's up to you how to manage multiple validators — with a simple Promise.all()
or some complex asynchronous sequences — as long as validator returns a single Promise.
To use a validator you should just pass it in a validator
prop to an individual field:
import { requiredValidator } from '../validators'
// …
<form>
<MyInput name="user.name" validator={requiredValidator} />
<MyInput name="user.status"/>
<MyInput name="user.friends.0"/>
</form>
// …
But this is just like my entire form is a single component with a single
onChange
!
Right.
Does it affect performance because of re-rendering entire form on every field change?
Probably in some cases it does. But as it was mentioned here before consider using Recompose pure()
HOC or React.PureComponent
to avoid that.
What about Redux?
Absolutely same approach: call an action on form onChange
and then use plain/immutable helper to return updated data state from a reducer.
This is a monorepo composed of these packages:
package | version | description |
---|---|---|
neoform | Core toolkit with form and field HOCs |
|
neoform-validation | formValidation and fieldValidation HOCs |
|
neoform-plain-object-helpers | getValue and setValue helpers for plain object state |
|
neoform-immutable-helpers | getValue and setValue helpers for Immutable state |
- Create a new folder in
packages/
, let's sayneoform-foo
. - See
package.json
in already existing packages and create newneoform-foo/package.json
. - Put source code in
neoform-foo/src/
, it will be transpiled and bundled intoneoform-foo/dist/
,neoform-foo/lib/
andneoform-foo/es/
. - Put tests written with Jest in
neoform-foo/test/
. - Put demo in
neoform-foo/demo/
, it will be rendered and wrapped with HMR.
Available scripts using Start:
yarn start build <package>
yarn start demo <package>
yarn start test
yarn start testWatch
yarn start lint
Available demos:
yarn start demo neoform
yarn start demo neoform-validation