Large Dynamically Generated Forms in React — Part 3

In part 3 we are going to learn about some of the proposed tools in Hooks as we attempt to convert our dynamic form. Our goal is to migrate all of our class components to function components while not losing any of the functionality or rendering work we have done in the last two parts.


Note:

At this time Hooks are only a proposal and are expected to change. The React team recommends not putting these into production.

You should take the time to fully read the React docs on the hooks proposal. Like the rest of the React docs, they are written very well and very actively maintained.

Also please don’t take this post as a suggestion you shouldn’t use class components. Class components still have their place and this isn’t to suggest that function components are better or the only way to go. Use what makes sense for use but know these tools exist.


Convert Components

We are going to walk through each component and convert them to function components. While doing that, I will talk through each new tool I am using and link to the docs page.

App

Simply converted to a function:

const DynamicFormOrganizedWithContextHooks = () => (
<FormDataProvider>
<Form />
</FormDataProvider>
)

Form

In our form, we get to see the use of some new tools. We use memo and useContext. memo was released as a part of React 16.6 as a solution to the problem of function components re-rendering even when props weren’t changing. memo allows us to get the same benefit of PureComponents in function components.

React.memo is a higher order component. It’s similar to React.PureComponent but for function components instead of classes.

One thing we will notice later is that at the time of writing this memo appears to mask the component name when using React Profiler but a fix is on its way.

useContext is the first hook we are going to use. It functions exactly as static contextType did in our class components, expect this hook allows us to use multiple contexts in our component.

const Form = memo(() => {
const context = useContext(FormDataContext)
  const handleSubmit = (e) => {
e.preventDefault()
console.log('Submitting Form! Form data:', context.formData)
}
  return (
<form style={styles.form} onSubmit={handleSubmit}>
<h1 style={styles.formTitle}>
Dynamic Form Organized With Context HOOKS!
</h1>
{albums.map((album) => {
const { albumId, albumName } = album
return (
<div key={albumId} style={styles.albumWrapper}>
<Title albumName={albumName} />
<Body albumId={albumId} />
</div>
)
})}
<button type="submit">Submit</button>
</form>
)
})

Title

Simply migrated to function component and used memo.

const Title = memo((props) => {
const { albumName } = props
return (
<div style={styles.titleWrapper}>
<h3 style={styles.title}>{albumName}</h3>
</div>
)
})

Body

Just like Title, Body migrated to function component and used memo.

const Body = memo((props) => {
const { albumId } = props
return (
<div style={styles.body}>
{fields.map((field) => {
const { fieldId, fieldName } = field
        return (
<div style={styles.formRow} key={fieldId}>
<label htmlFor={fieldId} style={styles.rowLabel}>
{fieldName}
</label>
<Input {...field} albumId={albumId} />
</div>
)
})}
</div>
)
})

Input

One again to keep things simple I am going to assume all our inputs are a text input. Our Input uses memo and useContext.

const Input = memo((props) => {
const context = useContext(FormDataContext)
const { fieldId, albumId } = props
const { formData, setState } = context
const stateKey = `${albumId}_${fieldId}
return (
<input
type="text"
id={fieldId}
style={styles.textInput}
onChange={(event) =>
setState({
[stateKey]: event.target.value,
})
}
value={formData[stateKey]}
/>
)
})

Context

Migrating the context provider to a function component required another few new hooks. We still use createContext all the same. For our context provider, we get to use 2 new hooks,useMemo and useState.

useMemo is nice and simple as it just memoizes a value. This is useful in our case because we aren’t interested in our initial state being recomputed each render. The second argument in useMemo is used to determine when the value should be recomputed. The value is only recomputed when at least one of the values in the second arguments have changed.

useState is a hook that recreates the ability a Component has to hold and update state. Here the first array destructed value is the “state” value and the second is the “setState” function. These can be named anything you would like but to keep things simple I left them as state and setState.

Note
Unlike the setState method found in class components, useState does not automatically merge update objects.

Just like suggested in the docs, I created the handleSetState function to make the act of setting state identical to the Input component.

export const FormDataContext = React.createContext()
const computeInitialState = () => {
const data = {}
for (let i = 0; i < albums.length; i++) {
for (let j = 0; j < fields.length; j++) {
data[`${albums[i].albumId}_${fields[j].fieldId}`] = ''
}
}
return data
}
export const FormDataProvider = (props) => {
const formData = useMemo(
() => computeFormData(),
[albums, fields]
)
  const [state, setState] = useState({ computeInitialState })
  const handleSetState = (object) => {
const { formData } = state
setState({
formData: { ...formData, ...object },
setState
})
}
const value = { ...state, setState: handleSetState }
  return (
<FormDataContext.Provider value={value}>
{props.children}
</FormDataContext.Provider>
)
}

Let make sure our components still re-render as this did at the end of Part 2. First without memo so we can see the components names and the entire tree re-rending and then second with memo.

React Profiler — Using Hooks without memo
React Profiler — Using Hooks with memo

Success! We have not recreated our dynamic form with the same rending as before.


In part 4, we are going to close all of this out by trying to stress test these implementations and see what sort of rough data we can get to see how much these steps can improve things.


Source code for this entire series can be downloaded here.