Force PushedFP
  • Home
  • Blog
  • Workbooks

Designing a Google Address Autocomplete Input in React / Redux

One of the first React controls I made to integrate with 3rd party libraries was a Google Address Autocomplete input.

I had one example to draw experience and lessons learned from, so I wasn't just starting from a blank slate. However, the example I could reference was really just a rough copy-paste from what looked like a Google document or Stackoverflow post. The example didn't really use a lot of React but was more of just using a JSX component to reference a mounting point for the Google Places library to do it's thing.

Using Google Places dropin code

Here was the example I found. I've simplified this a bunch from what it actually is since there's a lot of implementation in there that isn't relevant to this project and is just going to create confusion.

This example is super simple, but it leaves the user with a really basic UI that doesn't potentially match with the styles of the parent application.

import React from 'react'

class AutocompleteAddressInput extends React.Component {
  constructor(props) {
    super(props)
    this.autocomplete = undefined
  }

  componentDidMount() {
      this.bindAutocomplete()
  }

  render() {
    return (
      <input
        ref='autocomplete'
        type='text'
        value={this.props.readableAddress}
      />
    )
  }

  bindAutocomplete() {
    if (typeof google === 'undefined') {
      return
    }

    this.autocomplete = new google.maps.places.Autocomplete(
      this.refs.autocomplete,
      {
        componentRestrictions: {
          country: ['us', 'ca'],
        },
        types: ['establishment', 'geocode'],
      },
    )

    this.autocomplete.addListener('place_changed', () => {
      this.props.onSetJobsite(this.autocomplete.getPlace())
    })
  }
}

export default AutocompleteAddressInput

As you can see, the only real JSX here is:

<input
  ref='autocomplete'
  type='text'
  value={this.props.readableAddress}
/>

with an older type of React ref, mind you.

The Google autocomplete control is mounted in the React componentDidMount lifecycle method:

this.autocomplete = new google.maps.places.Autocomplete(
  this.refs.autocomplete,
  {
    componentRestrictions: {
      country: ['us', 'ca'],
    },
    types: ['establishment', 'geocode'],
  },
)

Then, there's a callback firing when a user clicks an autocomplete suggestion:

() => {
 this.props.onSetJobsite(this.autocomplete.getPlace())
}

While it is simplistic, it leaves all of the rendering work to the Google JS, putting the React virtual DOM in a weird state. There's probably some rendering deficiencies here, but other than that the implementation is complete.

Using Custom code

Now that I've shown you what a simple approach to this solution is relying on Google for rendering, I'd like to show you how I accomplished it.

Overall, this implementation feels much better and lets me use the same styling as the app and not be left with the Google styling.

import React from 'react'

class AutocompleteAddressInput extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            isListViewVisible: false,
        }
    }

    render() {

        return (
            <div>
                <input
                    onChange={this.props.onChangeAddress}
                    onFocus={() => {
                        this.setState({
                            isListViewVisible: true,
                        })
                    }}
                    value={value}
                />
                <div>
                    {
                      typeof suggestions !== 'undefined' && suggestions.length > 0
                        ? suggestions.map((suggestion) => (
                            <div
                                focusIdx={this.state.focusIdx}
                                onClick={(index) => {
                                  this.setState(
                                      {
                                          isListViewVisible: false,
                                      },
                                      () => {
                                          this.props.autocompleteIndexSelected(index)
                                      }
                                  )
                                }}
                            >
                              {suggestion.description}
                            </div>
                          ))
                        : false
                    }
                </div>
            </div>
        )
    }
}

export default AutocompleteAddressInput

A Redux container helps to manage the Google APIs state in a reducer

This also helps keep all of the Google stuff out of JSX and into something more appropriate for state management.

class Container extends React.Component {

    // ...bind actions etc here (boilerplate)

    componentDidMount() {
        // Load Google APIs
        const ggApiKey = // get Google API key from somewhere (JS payload, DOM meta etc)
        if (typeof ggApiKey !== 'undefined' && ggApiKey !== '') {
            const script = document.createElement('script')
            script.type = 'text/javascript'
            script.onload = function () {
                const googleMaps = window.google && window.google.maps
                if (!googleMaps) {
                    throw new Error('The Google Maps Places API was not found on the page.')
                }

                // Store our Google APIs state in the Redux reducers
                this.props.dispatch(
                    Actions.setGoogle({
                        autocompleteService: new googleMaps.places.AutocompleteService(),
                        geocoder: new googleMaps.Geocoder(),
                        googleMaps: googleMaps,
                    })
                )
            }

            document.getElementsByTagName('body')[0].appendChild(script)
            script.src = `https://maps.googleapis.com/maps/api/js?key=${ggApiKey}&v=3&libraries=places`
        }
    }
}

I needed to create some Redux actions for the JSX to interact w/ the Google state

These would just be bound by some boilerplate code in the Redux container. That part of the implementation should be obvious to you if you've ever worked with Redux.

This is basically where all of the interactions with the Google APIs will take place instead of in a React lifecycle method inside the JSX.

The important part I liked was that I could handle errors and different states of the autocomplete data, and it could be more readily available throughout the application as well without having to promote the data up to parents.

const autocompleteIndexSelected = (action, dispatch, state) => {
    const { app } = state
    const { google } = app

    if (typeof action.selectionIndex === 'undefined') {
        throw new Error('No autocomplete selection index provided')
    }

    const suggestion = app.suggestions[action.selectionIndex]

    if (typeof suggestion.place_id === 'undefined') {
        throw new Error('No Google API place ID provided')
    }

    const addressObj = {
        placeId: suggestion.place_id,
    }

    if (typeof google === 'undefined' || typeof google.geocoder === 'undefined') {
        throw new Error('Google API not available for geocoding usage')
    }

    google.geocoder.geocode(addressObj, (results) => {
        if (typeof results !== 'undefined' && results !== null && results.length > 0) {
            dispatch(Actions.updateSelectedJobsiteAddress(results[0]))
            dispatch(Actions.getCurrentJobsiteDateInfo(results[0]))
        }
        dispatch(Actions.setAutocompleteSuggestions([]))
    })
}
const onChangeAddress = (action, dispatch, state) => {
    const { app } = state
    const { google } = app

    if (typeof action.value === 'undefined' || action.value === '') {
        dispatch(Actions.setAutocompleteSuggestions([]))
        return
    }

    if (typeof google === 'undefined' || typeof google.autocompleteService === 'undefined') {
        throw new Error('Google API not available for autocomplete usage')
    }

    google.autocompleteService.getPlacePredictions(
        {
            componentRestrictions: { country: 'USA' },
            input: action.value,
        },
        (suggestions) => {
            if (typeof suggestions !== 'undefined' && suggestions !== null) {
                dispatch(Actions.setAutocompleteSuggestions(suggestions))
            }
        }
    )
}