iTranslated by AI
Building an Address Autocomplete Form with Remix and Conform
What is this?
I wanted to create a form that autocompletes an address from a postal code using Remix and a very easy-to-use form validation library, Conform.
It's one of those common features. It autocompletes a Japanese address based on the postal code.

I had to use Conform's Intent button and got a bit stuck, but I was able to solve it with help from the author of Conform on X. I will summarize the final result and the steps I took to solve it.
Demo of the finished result
Here is the demo:
Code Explanation
The following is the explanation.
Registration form with "Address Search" button
If we extract only the JSX for the form part, the code looks like this:
<Form method="POST" className="flex flex-col gap-4" {...getFormProps(form)}>
<div>
<Label htmlFor={zip1.id}>Postal Code</Label>
<HStack>
<div>
<Input className="w-16" {...getInputProps(zip1, { type: 'tel' })} />
<div className="text-sm text-destructive">{zip1.errors}</div>
</div>
<div>-</div>
<div>
<Input className="w-24" {...getInputProps(zip2, { type: 'tel' })} />
<div className="text-sm text-destructive">{zip2.errors}</div>
</div>
<Button
type="button"
variant="outline"
className="whitespace-nowrap"
onClick={() => fillAddressByPostalCode()}
>
Search Address
</Button>
</HStack>
</div>
<div>
<Label htmlFor={prefecture.id}>Prefecture</Label>
<Input {...getInputProps(prefecture, { type: 'text' })} />
<div className="text-sm text-destructive">{prefecture.errors}</div>
</div>
<div>
<Label htmlFor={city.id}>City/Municipality</Label>
<Input {...getInputProps(city, { type: 'text' })} />
<div className="text-sm text-destructive">{city.errors}</div>
</div>
<div>
<Label htmlFor={street.id}>Street Address</Label>
<Input {...getInputProps(street, { type: 'text' })} />
<div className="text-sm text-destructive">{street.errors}</div>
</div>
<Button className="mt-2 w-full">Register</Button>
</Form>
Forms built with Conform are great because they can be written concisely and are easy to read!
UI components such as Label, Input, and Button use shadcn-ui.
getFormProps and getInputProps are utility functions from Conform.
The fillAddressByPostalCode function, called by the onClick of the address search button, handles the process of autocompleting the address from the postal code.
Fetching the address from the postal code and reflecting it in the form
Before diving into the fillAddressByPostalCode function, let's first prepare a lookupAddress function that returns an address from a postal code string as a preliminary step.
For this, we'll use the Postal Code REST API, which is free for commercial use and offers unlimited usage. This API is developed and provided individually by Yuki Matsuzaka. Thank you!
const lookupAddress = async (postalCode: string) => {
const res = await fetch(`https://postcode.teraren.com/postcodes/${postalCode}.json`).catch(() => null)
if (!res || !res.ok) return null
return (await res.json()) as {
prefecture: string
city: string
suburb: string
street_address: string | null
}
}
Now, for the crucial fillAddressByPostalCode function. This is defined within the same route function component as the form.
// value needs to be referenced during rendering, otherwise it won't be updated and becomes undefined, so reference it here
// ref: https://github.com/edmundhung/conform/pull/467
const postalCode = `${zip1.value}${zip2.value}`
const fillAddressByPostalCode = async () => {
const address = await lookupAddress(postalCode) // Get address from postal code
if (!address) return
// Reflect address in form
form.update({
name: prefecture.name,
value: address.prefecture,
})
form.update({
name: city.name,
value: `${address.city}${address.suburb}`,
})
form.update({
name: street.name,
value: address.street_address ?? '',
})
toast.info('Address updated based on postal code')
}
form.update is a helper function that implements the Form Controls feature within Conform's Submission Intent.
Description
Conform utilizes the submission intent for all form controls, such as validating or removing a field. This is achieved by giving the buttons a reserved name with the intent serialized as the value. To simplify the setup, Conform provides a set of form control helpers, such as form.validate, form.reset or form.insert.
Japanese translation
Conformは、バリデーションやフィールドの削除など、すべてのフォームコントロールにサブミッションインテントを利用します。これは、インテントを値としてシリアライズした予約名をボタンに与えることで実現されます。設定を簡単にするために、Conformはform.validate、form.reset、form.insertなどのフォームコントロールヘルパーを提供しています。
This helper function is attached to the form variable obtained through useForm, and allows you to programmatically update form values. In this case, we used form.update to update the values of the controls.
One thing to keep in mind is that the name property passed to the form.update function must match the name property of the field. In this case, we use the name property belonging to each field variable (prefecture, city, street) also obtained from useForm. Since they contain the same strings as the variable names, you could also pass them as strings.
With this, the form that autocompletes the address from the postal code is complete. I'll omit the action that receives the form submission as it follows the standard pattern.
Pitfalls and Solutions
As mentioned briefly in the comment where the postalCode variable is assigned at the beginning of the previous code snippet, I encountered two pitfalls during implementation. Both were related to Conform's Intent Button. Here are the details.
Pitfall 1: Unable to get input values in the button's click handler
Initially, I was directly referencing the value from the field metadata within the fillAddressByPostalCode function called by the "Search Address" button's click handler, as shown below:
// Get address from postal code
const fillAddressByPostalCode = async () => {
const address = await lookupAddress(`${zip1.value}${zip2.value}`)
...
}
I was struggling because this was returning undefined. When displaying it inside the Form, the value was there, but it became undefined when accessed within lookupAddress. The behavior felt inconsistent.
So, I vented my frustration on X (formerly Twitter), and the author, Edmund, replied to me.
Based on his advice, I solved the issue by ensuring the following code is executed every time the component renders and using that value instead.
// value needs to be referenced during rendering, otherwise it won't be updated and becomes undefined, so reference it here
// ref: https://github.com/edmundhung/conform/pull/467
const postalCode = `${zip1.value}${zip2.value}`
The cause was exactly as explained in this pull request:
Conform uses useSyncExternalStore with subjects tracked by a proxy to achieve fine-grained subscription. This works fine generally.. but not in a callback the way you might want!
...
To solve this, a kinda awkward approach is to explicitly subscribe it during render:
Japanese translation
Conformはきめ細かいサブスクリプションを実現するために、プロキシによって追跡されたサブジェクトでuseSyncExternalStoreを使用します。これは一般的には問題なく動作しますが、あなたが望むようなコールバックでは動作しません!
...
これを解決するには、レンダリング中に明示的にサブスクライブするというちょっと厄介な方法がある:
I learned for the first time that React has a hook called useSyncExternalStore. It seems that while this hook allows reading input values managed in the DOM, they won't be updated and will return undefined unless they are referenced during each render. Wow, that's complex!
So, as a workaround for now, by assigning the postal code to a variable during rendering, we subscribe to it so it gets updated, and then by using that variable in the click handler, it should work fine.
Furthermore, this pull request considers two patterns for a fundamental solution.
One is a proposal to add fields.[name].latest.value, which would always be updated even in click handlers.
The other is to make fields.[name].value always update without using useSyncExternalStore (though this might have implications for Suspense?).
I didn't quite understand what "tearing" is, so it was a bit difficult for me. However, since it's unlikely that I'll be using Suspense for forms, and having two similar things seems confusing from a user's perspective, I felt that the second option (always updating) would be more convenient. I've left a comment to that effect, even though I don't fully grasp the technicalities. React and the DOM are truly deep...
Pitfall 2: Console warnings in react@canary
In Remix, which performs server-side rendering, there are cases where browser extensions rewrite the HTML, leading to hydration errors. It looks like this:

Checking the link confirms it is a Hydration Error.
This is an issue with React itself. While it has already been resolved in the latest branch, we've been stuck with React 18.2 for about two years now. It seems React 19 is likely to be released sometime this year, though.
To resolve these hydration errors, I always use react@canary. Since Next.js App Router and others seem to use canary normally without issues, I thought it would be fine. However, only in development mode with react@canary, a warning like this appeared for fields targeted by form.update when using Conform's getInputProps:

It seems to be saying "don't specify key= within spread syntax."
So, I initially tried manually specifying the field metadata's initialValue as the key after getInputProps, like this:
<Label htmlFor={prefecture.id}>Prefecture</Label>
<Input {...getInputProps(prefecture, { type: 'text' })} key={prefecture.initialValue} />
While the warning disappeared and it seemed to work, I ran into a problem where if I performed one postal code completion, modified the address slightly, and then tried to perform another completion, the value would not update. So close!
But it makes sense. The first time, the initialValue is updated after fetching the address, so it renders as a different component and updates. However, on the second update, the initialValue doesn't change, so the component doesn't update.
I considered various options, like creating a unique ID with useState to use as a key, but I didn't want to add unnecessary variables. While I was venting about this on X, Edmund helped me out once again.
Ultimately, since getInputProps already returns a key, the solution was simply to stop manually specifying key=. This key is generated internally by Conform during every update, making it exactly what was needed.
It seems the warning appears in the react@canary development environment, but it might just be a bit overzealous. Apparently, it has appeared in Next.js as well.
For this issue, Edmund went as far as cloning my repository locally, running it, fixing it, and even recording a video to show me. I am incredibly grateful.
Conform is great
All in all, Conform is an excellent library, and that includes its author. It's convenient, allows for concise code, and the creator is wonderful. It's the best, isn't it?
Source Code
The full source code for the demo above can be found here:
Discussion