3/23/22
Integrating React Hook Form & Redux-Toolkit
By Oren Farhi
When I'm coding forms with react, I prefer using react-hook-form. I find it simple but yet powerful enough. In one of the projects I was working on, the initial form's data was pulled from a redux store. The requirement for this project is to push the form's input back to the store - that means, submitting the form results in updating the store.
Some of the data in the store is not reflected in the form, but rather is updated directly with the reducer's action functions. In addition to that, data in the store may be updated from other sources that are not part of the form.
That means the form's values need to be updated with the latest data from the store, so updates on the data source should go both ways:
- from store to form
- from form to store
If not done right, these updates may go into an infinite loop which would crush the browser. I have come up with a strategy to make sure this would not happen.
The App with Redux Toolkit and useForm
ReadM includes a book editor (left image) and front page editor (right image) sections which allow an Editor to create books. This section is implemented with useForm and Redux-Toolkit.
Next, we're going to create our own custom hook which will use useForm and is responsible for updating the store or the form when either is changing - without going into an infinite loop.
Custom Form Hook
I want this custom form hook to be responsible for any data updates going both ways, and that's why I prefer to compose the logic inside this custom hook. I wrote before on improving your developer experience custom hooks.
This hook creates an instance of useForm and invokes two additional hooks which are responsible for updating the store and the form when the data changes in one of them. These hooks are actually standalone hooks and require the useForm api to perform the update.
export function useBookForm(book: IBook) {
const bookActions = useBookActions(book.id)
const useFormApi = useForm<IBook>({
defaultValues: book
})
const { handleSubmit } = useFormApi
/* Data source updaters */
// update the FORM => WHY? change comes from "book" prop
useFormUpdater(book, useFormApi.setValue)
// update the STORE => WHY? change comes from the "form"
useBookUpdater(useFormApi, bookActions.updateBook)
const onSubmitHandler = useCallback(
() => handleSubmit(bookActions.updateBook),
[handleSubmit, bookActions.updateBook]
)
return {
onSubmitHandler,
useFormApi
}
}
Update Form When The Data Source in Redux Store has Changed
The useFormUpdater hook is using the excellent useDeepCompareEffect hook which performs a deep equality comparison of the next and previous book object (a complex json object).
When change is detected, the useForm.setValue() is used to update the form. according to the [use form docs], setValue is more performant then reset() by avoiding "unnecessary re-rerenders". The shouldDirty property is set to false because in this case, I don't want the field to be set as if the change was coming from the form.
import { useDeepCompareEffect } from "react-use"
const useFormUpdater = (book: IBook, setValueToKey) => {
useDeepCompareEffect(() => {
const setValueToKey = ([key, value]: [string, any]) => {
setValue(key, value, { shouldDirty: false })
}
Object.entries(book).forEach(setValueToKey)
}, [book, setValue])
}
Update Redux When The Form has Changed
Next, this custom hook is responsible for watching the form's changes and updating redux. A naive approach would just invoke the redux action update function with the entire form's data - this would be prawn to that infinite re-rendering cycle I mentioned at the beginning of this article.
The solution for that is to update the store with the changes only. That means creating a book object with the properties that have changed.
This hook is using the useDeepCompareEffect to compare the entire book's form changes. However, in this case, I'm creating the set of changes by filtering the fields that have changed with the useFormApi.formState.dirtyFields. This is a key/value object (name of field, boolean value for "changed" status) that includes the fields that have changed only.
import { useDeepCompareEffect } from "react-use"
const useFormUpdater = (book: IBook, setValueToKey) => {
useDeepCompareEffect(() => {
const setValueToKey = ([key, value]: [string, any]) => {
setValue(key, value, { shouldDirty: false })
}
Object.entries(book).forEach(setValueToKey)
}, [book, setValue])
}
And that's what it takes to add redux as a caching layer for react-hook-form.
Usage of the custom Form hook in React
The usage of the custom useBookForm hook is simple and follows the useForm api. Since it simply exposes the entire form's api, we can use any of the exported functions of useForm and don't have to worry about updating the store for each field.
const useBookUpdater = (useFormApi, onChange) => {
const bookChanges: Partial<IBook> = useFormApi.watch()
const dirtyFields = useFormApi.formState.dirtyFields
useDeepCompareEffect(() => {
if (!isRecordEmpty(bookChanges)) {
const book = createBookFromDirtyFields(dirtyFields, bookChanges)
onChange(book)
}
}, [bookChanges])
}
This is just a way to achieve that and a solution I have come up with to solve that form hook and redux integration challenge.
Thanks for reading!
About the Author
Oren Farhi is a Senior Front End Engineer, Tech Lead and founder of ReadM. He graduated with a BA in Computer Science and Business Management from the Open University and has earned Software Engineering and Development skills by being self-driven and making things happen by experimenting, building and open sourcing some of his work, writing about it and reading a lot.
Oren believes in producing easy maintainable code for applications. He follows the principles of reactive programming, best practices of software architecture, and by creating modular and high quality testable products. He likes to keep code and app structure organized to let other developers easily understand and further extend it. Oren is proficient with front-end development and is working with various languages and frameworks such as React, Typescript, Redux, Cypress, Sass, Node.js and useful JavaScript based build tools that solves challenges well.
Aside from exploring Web Applications Engineering and sharing technical articles at Orizens.com, Oren enjoys spending time with his family, playing guitar, reading self-help books, enjoying nature and traveling.
This article was contributed by Oren Farhi, author of Reactive Programming with Angular and ngrx.