Design patterns in software are helpful for many reasons, but two of the most important are that they give developers a shared vocabulary and offer solutions to commonly encountered problems. They are popularly used in OO languages but not so often thought of in React projects.
In this article, I want to explore the Adapter pattern in React. In case you're not familiar with this pattern, I'll start with the classic object-oriented use case as an introduction. Then, I'll introduce an example of the Adapter pattern in action in a popular React library.
By the end of the article, I want to be able to answer these two questions:
With that, let's get started!
I'll be referring to the Adapter Pattern page from Refactoring Guru in this section. If you've never explored Refactoring Guru, it's a fantastic resource and is well worth your time.
From this article, we can extract some key points to help us define the Adapter pattern:
The example used by Refactoring Guru is one where you have a source of stock data in XML format and an analytics library that only takes in JSON data. The solution using the Adapter pattern is to wrap every interaction with the analytics library to take in the XML data, convert it to JSON, and send it to the appropriate methods of the library.
Now that we've defined the Adapter pattern let's find a React example in the wild.
I use react-hook-form quite often because it simplifies the process of creating forms in React projects. It leans into normal HTML standards when it comes to form submissions and validation, which makes it feel natural to use.
The most basic usage of react-hook-form involves the useForm
hook, which returns a register
function and a handleSubmit
function. You connect the handleSubmit
to the onSubmit
event on your form and connect the register
function to your form fields. I've linked to a CodeSandbox below, where I've set up a very basic form to illustrate this setup.
As you can see in the example, the register
function itself is designed to be used with uncontrolled inputs that have props for onChange
, onBlur
, name
, and ref
.
const { onChange, onBlur, name, ref } = register('firstName');
// include type check against field path with the name you have supplied.
<input
onChange={onChange} // assign onChange event
onBlur={onBlur} // assign onBlur event
name={name} // assign name prop
ref={ref} // assign ref prop
/>
// same as above
<input {...register('firstName')} />
What if you're trying to use a 3rd party or custom input that is controlled and has non-standard event handlers? Luckily for you, react-hook-form includes the Controller
component that is designed for such use cases. This Controller
component is an example of the Adapter pattern in action because it has the ability to wrap any component with non-standard form behavior and make it compatible with other standard form fields.
Note one of our takeaways from the Adapter Pattern article from Coding Guru above:
The Adapter pattern allows objects (or components) with incompatible interfaces to communicate with each other.
Here, the Controller
component allows react-hook-form
to communicate with basically any component that you would want to use in a form.
To see this in action, let's add on to the previous example form by adding a component to enter the username for the new user. Let's also pretend that we have some pre-existing component called UsernameInput
that automatically checks to see if this username already exists in the system. This component is controlled since it automatically checks the entered username with the backend on every keystroke. It also has an onValidUsername
prop that it only calls when a valid, available username has been entered.
In order to use this component in our form powered by react-form-hook
, we'll have to use the Controller
component to adapt the behavior of the UsernameInput
component to be compatible with the form.
<Controller
control={control}
rules={{ required: true }}
render={({ field }) => (
<UsernameInput
onValidUsername={(username: string) => {
field.onChange(username);
}}
/>
)}
name={"username"}
/>
In the above snippet, you can see where we've connected the field.onChange
function from the render prop of the Controller
component to the onValidUsername
prop of our UsernameInput
component.
This brings up a key feature of using the Adapter pattern in React. Render props! Render props make your adapter infinitely more reusable because it allows the adapter to wrap any component. Imagine a different Controller
component that simply wrapped the form component instead of providing a render prop. In this case, you would have to have a different Controller
component for every form component that needed adapting.
What signals in React apps point towards using the Adapter pattern? I think the most obvious is integrating 3rd party components into your application. Let's look at the MUI Datepicker as an example. This datepicker only accepts Date
objects as props, but what if your server sends all dates as UTC timestamps? One possible solution using the Adapter pattern would look something like this:
<UtcTimestampAdapter
timestamp={timestamp}
render={(localDate: Date) => (
<Datepicker defaultValue={localDate} />
)}
/>
In this example, the UtcTimestampAdapter
would handle converting the timestamp in UTC to the equivalent Date
in the user's timezone, and pass it through the render prop where you can render the Datepicker
. Now you can easily render the Datepicker
with the correct date throughout your application!
I hope after reading this article you can spot opportunities in your own React projects to use the Adapter pattern. You may even find that you've been using the pattern without realizing it! The benefit of learning to recognize and use design patterns is that it gives you and your team a common vocabulary when discussing or reading code. If you found this article helpful, let me know, and I'll write about other design patterns in React.
Also published here.