Simple form implementation from scratch
Lets create a simple form, exploring function children and component cloning techniques along the way. Currently there are many libraries providing good form components, but creating such components from scratch could be a fun learning exercise which will provide as a good playground to play with different techniques. Also my other, sample Contact Us form project needed some form components, so I really had no choice. Source code discussed, along with Express/MySql server, could be found on Github.
Form components
Form components pose an interesting challenge, as multiple input components not only should be functional on their own, but should also interact with each other to support validation/network requests. To orchestrate such communication, we could create some React context, but this would be undesirable as one may wish to keep validation and presentation logic separate from each other. Luckily there are two alternative techniques, which could be helpful and which will be investigated here: functions as component children and component cloning.
Lets start from children as functions. To decouple presentation and validation, I created two components: Form
and Field
. Their job will be validation logic. Field
also will be responsible to layout form and will control child components:
const App = () => {
...............................
return (
<div className="App">
<header className="App-header">
<h1>Contact Us</h1>
<p>{last}</p>
<Form onSubmit={onSubmit}>
<Field label={"First name"} name="first_name" required>{({ fieldProps }) => <TextField {...fieldProps} />}</Field>
<Field label={"Last name"} name="last_name">{({ fieldProps }) => <TextField {...fieldProps} />}</Field>
<Field label={"Address"} name="address" >{({ fieldProps }) => <TextField {...fieldProps} />}</Field>
<Field label={"Phone"} name="phone">{({ fieldProps }) => <TextField {...fieldProps} />}</Field>
<Field label={"Email"} name="email" required email>{({ fieldProps }) => <TextField {...fieldProps} />}</Field>
<Field label={"Message"} name="message" required>{({ fieldProps }) => <Textarea {...fieldProps} />}</Field>
<input type="submit" value="Submit" />
</Form>
</header>
</div>
)
}
As you can see, <Field/>
component receives common field properties like label
and name
and passes them, possibly with some modifications, to children using fieldProps
field of a child function argument. Thanks to children functions, the main goal is reached: <Textarea/>
and <TextField/>
could be completely unaware of the validation.
As a side note, here we could use children cloning, which I will consider later. This is a valid alternative, but children functions give us additional flexibility, for example, if necessary, we can now change input components based on validation error (passed to children functions as another argument property).
Now, Form
and Field
components should be aware of each other, so we could use React context and children functions for even greater flexibility. But here I consider yet another, alternative technique instead: React cloning. The technique is quite old, and may seem a little bit hackish, but it could be a good alternative when components should interact with each other, still avoiding tight coupling:
export const Form = ({ onSubmit, children }) => {
const validation = useRef({})
const values = useRef({})
const [errors, setErrors] = useState({})
const setValidation = (name, validator) => {
validation.current = { ...validation.current, [name]: validator }
}
const setValue = (name, value) => {
values.current = { ...values.current, [name]: value }
}
const handleSubmit = event => {
event.preventDefault()
const errors = Object.entries(validation.current).reduce((errors, [name, validator]) => {
const schema = joi.object({ [name]: validator })
const { error } = schema.validate({ [name]: values.current[name] })
if (error) {
errors[name] = error.message
}
return errors
}, {})
setErrors(errors)
if (Object.keys(errors).length) {
return
}
const schema = joi.object(validation.current)
const {value} = schema.validate(values.current)
if(onSubmit) {
onSubmit(value)
}
}
const augmented = React.Children.map(children, child => {
if (child.type === Field) {
return React.cloneElement(child, { setValidation, setValue, errors })
}
return child
})
return (
<div className="Form">
<form onSubmit={handleSubmit}>
{augmented}
</form>
</div>
)
}
Here we call React.cloneElement(child, { setValidation, setValue, errors })
to add setValidation
, setValue
, and errors
properties to all Field
components. Here setValidation
is used to setup validation for each field, and setValue
- to update value on input element change event.
Pretty evidently, component cloning plays nicely with children function technique and could be extended to do deep recursive cloning.
Next
Speaking about interaction between components, it is impossible not to mention Redux. Tried and tested instrument, Redux, especially in combination with Redux Sagas, provides a clear, no-brainer tool to wire up applications of any complexity. As good and popular it is, nonetheless, it could be an overkill for simple components/applications.
Now, when form components are created, it could be instructive to look at how they could be used and tested in simple Contact Us form app.