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 toReact.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
.
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.