on
Using the Visitor Pattern with React to write even more reusable components
The cool thing about design patterns is that, even if you don’t read the Design Patterns Book, you’ll discover them yourself. In nature, natural selection causes unrelated species to converge on an optimal trait, as in the body shapes of dolphins, sharks, and penguins. Like animal species, we programmers converge on ways to solve common problems. Last week, after writing a login, signup, place-order, and edit-settings form, I got sick of writing the same React form over and over. While working out how to reuse the form code, it dawned on me that I was using the Visitor Pattern.
Here is stateless React component for a login form.
export function LoginForm(props: ILoginFormProps) {
return <form>
<div>
<label>Email address</label>
<input type="text" value={props.emailAddress} onChange={props.onChangeEmail}/>
</div>
<div>
<label>Password</label>
<input type="password" value={props.password} onChange={props.onChangePassword}/>
</div>
<div>
<input type="submit" value="Submit" onChange={props.onSubmit} />
</div>
</form>;
}
React makes writing components like forms very easy. But it is tedious writing out the same <div>
, <label>
, and <input>
elements for every new form. Smart programmes don’t repeat themselves. Let’s model the entire form with two new entities.
export interface IFormField {
getValue: () => string;
setValue: (val: string) => void;
label: string;
name: string;
type: string;
}
import * as React from 'react';
import {IFormField} from './IFormField';
export class Form {
public fields: IFormField[];
public onSubmit: () => void;
constructor(fields: IFormField[], onSubmit: () => void) {
this.fields = fields;
this.onSubmit = onSubmit;
}
}
This is an adequate model of a typical form. Let’s add a render()
method that converts it into JSX.
export class Form {
// ...
public render(): JSX.Element {
return <form onSubmit={() => false}>
{this.fields.map((formField: IFormField, i: number) => {
const onChange = (e: any) => formField.setValue(e.target.value);
return <div key={i}>
<label>{formField.label}</label>
<input type={formField.type} name={formField.name} value={formField.getValue()} onChange={onChange} />
</div>;
})}
<input type="submit" onClick={this.onSubmit} />
</form>;
}
}
Our Form is starting to look like a React component. It is pretty useful so far - we can use it anywhere a form might be needed. IFormField is an interface, so its implementation can have mutable data, or utilise a change function.
export class FluxFormField implements IFormField {
public label: string;
public name: string;
public type: string;
private value: string;
private onChange: (val: string) => void
constructor(type: string, name: string, label: string, value: string, onChange: (val: string) => void) {
this.type = type;
this.name = name;
this.label = label;
this.value = value;
this.onChange = onChange;
}
public getValue(): string {
return this.value;
}
public setValue(val: string): void {
this.onChange(val);
}
}
This is what our login form would look like.
function LoginForm(props: ILoginFormProps) {
const username = new FluxFormField('text', 'username', 'Username', (val: string) => props.actions.setFormField('username', val));
const password = new FluxFormField('password', 'current-password', 'Password', (val: string) => props.actions.setFormField('password', val));
const onSubmit = () => props.actions.submitLogin(username.getValue(), password.getValue())
const form = new Form([
username,
password,
], onSubmit);
return form.render();
}
Now we have a reusable component that we can configure for our particlar form. It doesn’t care how our data is stored. We can display a number input by setting IFormField.type
to “number”. But if we had a dropdown box behind the IFormField interface, we would need to change Form.render()
. We shouldn’t have to do that. So enter the visitor pattern.
export interface FormVisitor {
visitForm(form: Form): void;
visitFormField(formField: IFormField): void;
visitOnSubmit(onSubmit: (Form) => void): void;
}
export interface FormVisitable {
accept(visitor: FormVisitor): void;
}
If you don’t remember the visitor pattern exactly, it’s pretty easy to understand. The Open-closed principle should discourage us from adding new methods to a class once we nail its definition. The prevents us from adding non-cohesive methods like render
, toQueryString
, or toJSON
to our Form class. We define accept(visitor)
as an operation on the class to be visited. The visitee class takes his visitor on a tour of all his data, letting the guest do whatever she pleases with it.
export class Form {
// ...
public accept(visitor: Visitor) {
visitor.visitForm(this);
for (let f of this.fields) {
visit.visitField(f);
}
visitor.visitOnSubmit(this.onSubmit);
}
}
Now the visitor is responsible for the JSX.
export class ReactFormVisitor implements FormVisitor {
protected components: JSX.Element[] = [];
public visitForm(form: Form): void {}
public visitField(formField: IFormField): void {
let key = 0;
components.push(<div key={key++}>
<label>{formField.label}</label>
<input type={formField.type} name={formField.name} value={formField.getValue()} onChange={onChange} />
</div>);
}
public visitSubmit(onSubmit: () => void): void {
this.components.push(<input type="submit" onClick={onSubmit} value="Submit"/>
}
public render(): JSX.Element {
return <form onSubmit={() => false}>
{this.components}
</form>;
}
}
It’s easy to add a new implementation of IFormField. Consider OptionFormField which would model the <select>
element. You can edit the ReactFormVisitor to deal with the OptionFormField case. SOLID purists might subclass it as below. Said purists might object to the use of the “instanceof” operator. I hate it too. Unfortunately TypeScript doesn’t have elegant function overloading, so we can’t implement the Visitor pattern exactly by the book.
public ImprovedFormVisitor extends ReactFormVisitor {
public visitField(formField: IFormField): void {
if (formField instanceof OptionFormField) {
let options = formField.options.map((o: string) => <option value={o}>{o}</option>);
this.components.push(<select onChange={onChange}>{options}</select>);
} else {
super.visitField(formField);
}
}
}
OO paradigms may not be trendy in the React + TypeScript + Flux space. You might conclude that this approach is overkill. Perhaps there is a better way to write reusable forms and other UI components. At the end of the day, I am always looking for ways better ways to work, and I’m open to ideas. For now, I’m trying out this approach to save time and write more elegant code.