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. 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.onChangeHandler = this.onChangeHandler.bind(this);
}
onChangeHandler(name, value) {
this.setState((prevState) => setValue(prevState, name, value))
}
onSubmit() {
console.log(`Submitting ${this.state.data}`)
}
render() {
<MyForm
data={this.state.data}
getValue={getValue}
onChange={this.onChangeHandler}
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,
onSubmit
}) => (
<form onSubmit={(e) => {
e.preventDefault();
validate(onSubmit)
}}>
<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 provided callback (onSubmit
handler in most cases)validationStatus
–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) => {
if (value === '') {
return Promise.reject('💩');
}
return Promise.resolve('🎉');
};
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>
// …
For a better understanding, you can play with some examples in this repo:
yarn start demo neoform
yarn start demo neoform-validation
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 |
yarn start build neoform
yarn start build neoform-validation
yarn start build neoform-plain-object-helpers
yarn start build neoform-immutable-helpers
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.