Using react HOC with a form. Passing common JSX and state down using a HOC

Viewed 1802

I just finished up a pretty big project that uses a form in different parts of a react app. The form is identical throughout however the functionality is different depending on where the form is being used.

So right now I have multiple Components with duplicate forms. All of these forms are controlled i.e (use this.state... for the value) the only difference is the render methods and what happens to the form data on certain button events.

I realize this is horrible coding and really want to use a HOC to make these components way more flexible and cleaner.

this is one of the forms, There are 2 more similar to this.

sampleForm = (
            <form action="" name="sample">
                <div className="group">
                    <label htmlFor="">Descriptive Name:</label>
                    <input type="text" name="name" value={this.state.name}
                        onChange={this.handleChange} placeholder="Descriptive Name" />
                </div>
                <div className="group">
                    <label>Sample Codename:</label>
                    <input type="text" name="codeName" value={this.state.codeName}
                        onChange={this.handleChange} placeholder="Ex: MM_MG_01" />
                </div>
                <div className="group">
                    <label htmlFor="">GPS Coordinates:</label>
                    <input type="text" name="coords" value={this.state.coords}
                        onChange={this.handleChange} placeholder="GPS Coordinates" />
                </div>
                <div className="group">
                    <label htmlFor="">Metagenomic Data:</label>
                    <textarea type="text" name="METAdesc" value= 
                     {this.state.METAdesc}
                        onChange={this.handleChange} placeholder="Image Description" rows={7} />
                    <input type="file" name="METAimage"
                        onChange={this.handleChange} />
                </div>
                {notes}
            </form>
        )

I currently have these three duplicated in the render method ( four times :/ )

How can I pass these three down to components?

1 Answers

A Higher Ordered Component is a React pattern where:

a function takes a Component and returns a new Component. https://reactjs.org/docs/higher-order-components.html

One thing an HOC can do for a Form is to manage state, handling Events such as onChange and onSubmit, etc, etc. As such, consider a Form Component as a functional component that is passed as a parameter to your FormHandler HOC.

For instance,

In FormHandler.js

const withFormHandling = FormComponent => class extends Component {/* Component Logic */}
export default withFormHandling

In Form.js

import withFormHandling from './path/to/Components/FormHandler'
const Form = props => {/* Component Logic */}
export default withFormHanlding(Form);

How then do we handle form specifics, props and state for multiple different forms?

  • Identify the state or props that every form should have

In your case, perhaps the following:

formAction
formName
handleChange
handleSubmit
inputNames
notes
errors

I would consider passing in inputNames and errors as props (they should match in structure). You can add all sorts of complexity here or keep it simple. Personally, I keep in my state a fields object and an errors object, both with matching keys. One for maintaining user-entered values, the other for storing the results of field validation.

Let's then fill out our HOC

const withFormHandling = FormComponent => class extends Component {
    constructor(props) {
        super(props)
        this.state = {
            fields: {...props.inputNames},
            errors: {...props.errors},
            selectedFile: null
        }
        this.handleChange = this.handleChange.bind(this);
        this.handleFile = this.handleFile.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }
    // https://codeburst.io/save-the-zombies-how-to-add-state-and-lifecycle-methods-to-stateless-react-components-1a996513866d 
    static get name() {
        return Component.name;
    }

    handleChange(e) {
        const target = e.target;
        let value = target.type === 'checkbox' ? target.checked : target.value;
        let name = target.name;
        const fields = {...this.state.fields},  errors = {...this.state.errors};
        const error = //create a validation function that returns an error based on field name and value
        fields[name] = value;
        errors[name] = error;
        this.setState({ fields, errors })
    }

    handleFile(e) {
        this.setState({selectedFile: event.target.files[0]})
    }

    handleSubmit(e) {
        //validate form
        //flatten fields into structure of data for form submission
        //handle submission of data and also file data
    }

    render() {
        return <FormComponent 
                   {...this.props} //to access form specific props not handled by state
                   fields={this.state.fields}
                   errors={this.state.errors}
                   handleChange={this.handleChange}
                   handleFile={this.handleFile}
                   handleSubmit={this.handleSubmit}
               />
    }
}
export default withFormHandling

This Pattern works because the render function of the returned Component renders the Form Component passed as a parameter to the HOC function.

So, you can create any number of Forms with this HOC as the handler. You can consider passing into the HOC a tree representing the input structure of the form to make this even more modular and reusable.

For now, let's fill out Form.js for the example you provided:

import withFormHandling from './path/to/Components/FormHandler'
const Form = ({formAction, formName, handleChange, handleFile, handleSubmit, fields, errors, notes}) => {
    return (
        <form action={formAction} name={formName} onSubmit={handleSubmit}>
            <div className="group">
                <label htmlFor="name">Descriptive Name:</label>
                <input type="text" name="name" value={fields.name}
                    onChange={handleChange} placeholder="Descriptive Name" />
            </div>
            <div className="group">
                <label htmlFor="codeName">Sample Codename:</label>
                <input type="text" name="codeName" value={fields.codeName}
                    onChange={handleChange} placeholder="Ex: MM_MG_01" />
            </div>
            <div className="group">
                <label htmlFor="coords">GPS Coordinates:</label>
                <input type="text" name="coords" value={fields.coords}
                    onChange={handleChange} placeholder="GPS Coordinates" />
            </div>
            <div className="group">
                <label htmlFor="METAdesc">Metagenomic Data:</label>
                <textarea type="text" name="METAdesc" value= 
                 {fields.METAdesc}
                    onChange={handleChange} placeholder="Image Description" rows={7} />
                <input type="file" name="METAimage"
                    onChange={handleFile} />
            </div>
            {notes}
        </form>
    )
}
export default withFormHanlding(Form);

Finally, in some other Component, you call the Form Component as often as you like, passing in unique props.

//...some other Component Render Method

// form one, with its own internal state managed by HOC
<Form formAction={'https://someendpoint1'} formName={"some form 1"} inputNames={{name:'', codename:''}} errors={{name:'', codename:''}} notes={"some notes 1"}/>

// form two, with its own internal state managed by HOC
<Form formAction={'https://someendpoint2'} formName={"some form 2"} inputNames={{name:'', codename:''}} errors={{name:'', codename:''}} notes={"some notes 2"}/>

// form three, with its own internal state managed by HOC
<Form formAction={'https://someendpoint3'} formName={"some form 3"} inputNames={{name:'', codename:''}} errors={{name:'', codename:''}} notes={"some notes 3"}/>

This is how I have handled an app with many different forms that have a similar structure.

Another pattern to consider is render props, but I'll leave that up to another question and another respondent.

Related