Build address search component in React
Tags: nextjs, react, javascript, webdev
Published: 2021-09-12 06:28:47

---
canonical_url: "https://www.thangphan.xyz/posts/build-address-search-component-in-react/"
---
In this post, I'm going to build an address search component using React.

### Requirements
In the component, I have a postcode value, and every time I click the search button, I can get the address.
 
It will have two cases of the result:
1. Return one address.
2. Return many addresses.

If the result is one address, I will set the value for the addresses inputs(disabled) otherwise I have to show the address list for the user. When the user selects an address from the address list, the selected address will be showed in the address's inputs.

### Thinking in React
#### Components
My component will have 4 child components:
1. `PostCode`.
2. `Search`.
3. `Addresses`.
4. `Address`.

#### State
This is some states I need to have for my components:
1. `postCode` for `PostCode`.
2. `addresses` for `Addresses`.
3. `address` for `Address`.
4. `isOpen` for toggling the `Addresses` component.

#### Implement

![Address Implementation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ed90wk75smcabd82a9hv.png)

### Coding

I will use `Context` in order to avoid `props drilling` in React.

* `Search` component need to use these state: `postCode`, `address`, `addresses`, `isOpen`.
* `Addresses` component need to use state: `address`.

So I will have the `Providers` like below:
```tsx
<AddressProvider>
  <AddressesProvider>
    <IsOpenProvider>
      <PostCodeProvider>
        <PostCode />
        <Search />
      </PostCodeProvider>
      <Addresses />
    </IsOpenProvider>
  </AddressesProvider>
  <Address />
</AddressProvider>
```

I created a function that helps me for generating the context and a `hook` to consume that context.

```tsx
function createContext(name: string) {
  const context = React.createContext(null)

  function useContext() {
    const contextValue = React.useContext(context)

    if (contextValue === null) {
      throw new Error(`use${name} must be used within ${name}Provider`)
    }

    return contextValue
  }

  return {Context: context, useContext}
}
```
Now, I will create the provider above:

```tsx

type IsOpenContextType = {
  isOpen: boolean
  setIsOpen: React.Dispatch<React.SetStateAction<boolean>>
}
const {Context: IsOpenContext, useContext: useIsOpen} =
  createContext<IsOpenContextType>('IsOpen')

function IsOpenProvider({children}: {children: React.ReactNode}) {
  const [isOpen, setIsOpen] = React.useState(false)
  const value = React.useMemo(() => ({isOpen, setIsOpen}), [isOpen, setIsOpen])
  return (
    <IsOpenContext.Provider value={value}>{children}</IsOpenContext.Provider>
  )
}

type Address = {
  code: number
  prefecture: string
  city: string
  ward: string
}
type AddressContextType = {
  address: Address | null
  setAddress: React.Dispatch<React.SetStateAction<Address | null>>
}
const {Context: AddressContext, useContext: useAddress} =
  createContext<AddressContextType>('Address')

function AddressProvider({children}: {children: React.ReactNode}) {
  const [address, setAddress] = React.useState<Address | null>(null)
  const value = React.useMemo(
    () => ({address, setAddress}),
    [address, setAddress],
  )

  return (
    <AddressContext.Provider value={value}>{children}</AddressContext.Provider>
  )
}

type AddressesContextType = {
  addresses: Array<Address> | null
  setAddresses: React.Dispatch<React.SetStateAction<Array<Address> | null>>
}

const {Context: AddressesContext, useContext: useAddresses} =
  createContext<AddressesContextType>('Addresses')
function AddressesProvider({children}: {children: React.ReactNode}) {
  const [addresses, setAddresses] = React.useState<Array<Address> | null>(null)
  const value = React.useMemo(
    () => ({addresses, setAddresses}),
    [addresses, setAddresses],
  )

  return (
    <AddressesContext.Provider value={value}>
      {children}
    </AddressesContext.Provider>
  )
}
type PostCodeContextType = {
  postCode: Array<string>
  setPostCode: React.Dispatch<React.SetStateAction<Array<string>>>
}

const {Context: PostCodeContext, useContext: usePostCode} =
  createContext<PostCodeContextType>('PostCode')
function PostCodeProvider({children}: {children: React.ReactNode}) {
  const [postCode, setPostCode] = React.useState(() =>
    Array.from({length: 2}, () => ''),
  )

  const value = React.useMemo(
    () => ({postCode, setPostCode}),
    [postCode, setPostCode],
  )

  return (
    <PostCodeContext.Provider value={value}>
      {children}
    </PostCodeContext.Provider>
  )
}
```

`PostCode` component:

```tsx

function PostCode() {
  const {postCode, setPostCode} = usePostCode()

  function hanldePostCodeChange(
    event: React.ChangeEvent<HTMLInputElement>,
    idx: number,
  ) {
    const newPostCode = [...postCode]
    newPostCode.splice(idx, 1, event.target.value)
    setPostCode(newPostCode)
  }
  return (
    <div>
      <input onChange={(e) => hanldePostCodeChange(e, 0)} />
      <input onChange={(e) => hanldePostCodeChange(e, 1)} />
    </div>
  )
}
```

When I click the search button, I have to fake an API for it. I will use `msw`, and create a fake API.

```tsx
rest.get('/addresses', (req, res, ctx) => {
  return res(
    ctx.delay(3000),
    ctx.status(200),
    ctx.json({
      data: [
        {
          code: 13,
          prefecture: 'Tokyo',
          city: 'Otaku',
          ward: 'Kamata',
        },
        {
          code: 12,
          prefecture: 'Osaka',
          city: 'Namba',
          ward: 'Suidou',
        },
      ],
    }),
  )
}),
```

`Search` component:

```tsx

function Search() {
  const {setAddress} = useAddress()
  const {postCode} = usePostCode()
  const {setAddresses} = useAddresses()
  const {setIsOpen} = useIsOpen()

  async function handleAddressesSearch() {
    const query = postCode.every((pc) => Boolean(pc)) ? postCode.join('-') : ''
    if (!query) return

    const res = await fetch(`addresses?postCode=${query}`)
    const resJson = await res.json()

    if (resJson.data.length > 1) {
      setIsOpen(true)
      setAddresses(resJson.data)
    } else {
      setAddress(resJson.data[0])
    }
  }
  return <button onClick={handleAddressesSearch}>Search</button>
}
```

`Addresses` component:

```tsx

function Addresses() {
  const {addresses} = useAddresses()
  const {setAddress} = useAddress()
  const {isOpen, setIsOpen} = useIsOpen()

  function handleAddressSelect(address: Address) {
    setIsOpen(false)
    setAddress(address)
  }

  if (!isOpen) return null
  return (
    <ul>
      {addresses?.map((ad, _idx) => (
        <li
          key={`addresses-items-${_idx}`}
          onClick={() => handleAddressSelect(ad)}
        >
          {ad.prefecture},{ad.city}, {ad.ward}
        </li>
      ))}
    </ul>
  )
}
```

`Address` component:
```tsx

function Address() {
  const {address, setAddress} = useAddress()

  function handleWardChange(event: React.ChangeEvent<HTMLInputElement>) {
    setAddress({
      ...address,
      ward: event.target.value,
    })
  }

  return (
    <div>
      <input value={address?.code ?? ''} disabled />
      <input value={address?.prefecture ?? ''} disabled />
      <input value={address?.city ?? ''} disabled />
      <input value={address?.ward ?? ''} onChange={handleWardChange} />
    </div>
  )
}
```

And combine all of the components to my page:

```tsx
function AutoAddress() {
  return (
    <AddressProvider>
      <AddressesProvider>
        <IsOpenProvider>
          <PostCodeProvider>
            <PostCode />
            <Search />
          </PostCodeProvider>
          <Addresses />
        </IsOpenProvider>
      </AddressesProvider>
      <Address />
    </AddressProvider>
  )
}
```
Let's test my app with two cases:

- Result with one address:

![One Address](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vto9hnzy3eofljitphne.gif)


- Result with many addresses:

![Many Addresses](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5vh3716fit3gbakltrl9.gif)

### Improvement

My component worked as I expected, but it has a problem is every time I click the button search, I have to re-fetch API. It will be better if I can cache the `addresses` in case the `postCode` wasn't changed. 

And `swr` is very helpful in this case.

1. I need to have a flag `wasSearched` to make sure calling API only happens when I click the button search.
2. When I pass `query` of `postCode` to `useSWR`, `useSWR` will automatically determine the value of `postCode` changed or not.
3. I have to check if `postCode` wasn't changed for two cases(one address or many addresses), and do some stuff with each case.
 
Let's create `WasSearchedProvider`:

```tsx

type WasSearchedContextType = {
  wasSearched: boolean
  setWasSearched: React.Dispatch<React.SetStateAction<boolean>>
}
const {Context: WasSearchedContext, useContext: useWasSearched} =
  createContext<WasSearchedContextType>('WasSearched')

function WasSearchedProvider({children}: {children: React.ReactNode}) {
  const [wasSearched, setWasSearched] = React.useState(false)
  const value = React.useMemo(
    () => ({wasSearched, setWasSearched}),
    [wasSearched, setWasSearched],
  )
  return (
    <WasSearchedContext.Provider value={value}>
      {children}
    </WasSearchedContext.Provider>
  )
}
```

Change `Addresses` component code to using `swr`:

```tsx
function AddressesProvider({children}: {children: React.ReactNode}) {
  const {wasSearched} = useWasSearched()
  const {postCode} = usePostCode()
  const {setIsOpen} = useIsOpen()
  const {address, setAddress} = useAddress()
  const query = postCode.every((pc) => Boolean(pc)) ? postCode.join('-') : ''

  const {data: addresses, error} = useSWR(
    wasSearched ? `addresses?postCode=${query}` : null,
    (arg: string) =>
      fetch(arg)
        .then((r) => r.json())
        .then((res) => {
          if (res?.data.length === 1) {
            const {code, city, prefecture, ward} = res.data[0]
            setAddress({
              ...address,
              code,
              city,
              prefecture,
              ward,
            })
          }

          return res?.data
        }),
  )

  useDeepCompareEffect(() => {
    if (!addresses) return

    if (addresses.length > 1) {
      setIsOpen(true)
    }
  }, [{addresses}])

  const value = React.useMemo(() => ({addresses, error}), [addresses, error])

  return (
    <AddressesContext.Provider value={value}>
      {children}
    </AddressesContext.Provider>
  )
}
```

1. If the response is one address, I will set the state for `address` immediately.
2. If the response is many addresses, I will have to wait for the `addresses` state to be set, then trigger a compare between the current state and previous state using `useDeepCompareEffect`. If it is different, I will change `isOpen` from `false` -> `true`.

`Addresses` component also need to access these states: `postCode`, `wasSearched`, `isOpen`, `setAddress`. 

Let's change the `providers`:

```tsx
function AutoAddress() {
  return (
    <AddressProvider>
      <WasSearchedProvider>
        <PostCodeProvider>
          <IsOpenProvider>
            <AddressesProvider>
              <PostCode />
              <Search />
              <Addresses />
            </AddressesProvider>
          </IsOpenProvider>
        </PostCodeProvider>
      </WasSearchedProvider>
      <Address />
    </AddressProvider>
  )
}
```

Change the logic in `Search` component: 

```tsx

function TwoArrayStringIsEqual(a: Array<string>, b: Array<string>) {
  return a.every((str, idx) => str === b[idx])
}

function usePrevious<T>(value: T) {
  const ref = React.useRef(value)

  React.useEffect(() => {
    ref.current = value
  })

  return ref.current
}

function Search() {
  const {address, setAddress} = useAddress()
  const {postCode} = usePostCode()
  const previousPostCode = usePrevious<Array<string>>(postCode)
  const {addresses} = useAddresses()
  const {setWasSearched} = useWasSearched()
  const {setIsOpen} = useIsOpen()

  async function handleAddressesSearch() {
    setWasSearched(true)

    if (addresses && TwoArrayStringIsEqual(previousPostCode, postCode)) {
      if (addresses.length === 1) {
        const {code, city, prefecture, ward} = addresses[0]
        setAddress({
          ...address,
          code,
          city,
          prefecture,
          ward,
        })
      } else {
        setIsOpen(true)
      }
    }
  }
  return <button onClick={handleAddressesSearch}>Search</button>
}
```
When I change the `postCode` input, It will call API, because the value of `postCode` was changed. So I have to reset `wasSearched` to `false`.

```tsx
function hanldePostCodeChange(
  event: React.ChangeEvent<HTMLInputElement>,
  idx: number,
) {
  if (wasSearched) {
    setWasSearched(false)
  }
  const newPostCode = [...postCode]
  newPostCode.splice(idx, 1, event.target.value)
  setPostCode(newPostCode)
}
```
Now, I can see the address list immediately and don't need to trigger a fetch request.

![Addresses Caching](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q685nlz73xm0yiv971kd.gif)

Change the `postCode` and re-fetch API:

![PostCode Changed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ykznlmr8fj3digpv37b3.gif)

### Conclusion

I have just built an `address component` using React, and improve the performance by using `swr`. Please feel free to refer to the [source code](https://github.com/thangphan37/dev-address).