import * as React from 'react' ; import { AutocompleteProps } from '../index.d' ; import * as uuid4 from 'uuid/v4' ; const initialState = { results : { predictions : [ ] , status : '' , } , isLoading : false , error : null , } ; const cors = process . env . NODE_ENV !== 'production' ? 'https://cors-anywhere.herokuapp.com/' : '' ; export default function useGoogleAutocomplete ( { apiKey , query , type = 'places' , debounceMs = 400 , options = { } , } ) { const [ state , dispatch ] = React . useReducer ( reducer , initialState ) ; const sessionToken = React . useRef < string > ( uuid4 ( ) ) ; const sessionTokenTimeout = React . useRef < number > ( ) ; const abortController = React . useRef < any > ( ) ; const abortSignal = React . useRef < any > ( ) ; const placesAbortController = React . useRef < any > ( ) ; const placesAbortSignal = React . useRef < any > ( ) ; React . useEffect ( ( ) => { sessionTokenTimeout . current = window . setInterval ( resetSessionToken , 180000 ) ; abortController . current = new AbortController ( ) ; abortSignal . current = abortController . current . signal ; placesAbortController . current = new AbortController ( ) ; placesAbortSignal . current = placesAbortController . current . signal ; placesAbortController . current ; return ( ) => { clearInterval ( sessionTokenTimeout . current ) ; abortController . current . abort ( ) ; placesAbortController . current . abort ( ) ; } ; } , [ ] ) ; const initialRender = React . useRef < boolean > ( false ) ; const debouncedFn = React . useRef < any > ( ) ; React . useEffect ( ( ) => { if ( initialRender . current === false ) { initialRender . current = true ; return ; } if ( debouncedFn . current ) debouncedFn . current . clear ( ) ; if ( query . length === 0 ) { dispatch ( { type : 'INVALID_REQUEST' , } ) ; return ; } if ( ! state . isLoading && ! abortController . current . signal . aborted ) { dispatch ( { type : 'LOADING' , } ) ; } debouncedFn . current = debounce ( ( ) => { const types = options . types && type === 'places' ? `&types=${ options . types }` : '' ; const strictbounds = options . strictbounds && types === 'places' ? `&strictbounds` : '' ; const offset = options . offset && type === 'query' ? `&offset=${ options . offset }` : '' ; const language = options . language ? `&language=${ options . language }` : '' ; const location = options . location ? `&location=${ options . location }` : '' ; const radius = options . radius ? `&radius=${ options . radius }` : '' ; const url = `${ cors }https://maps.googleapis.com/maps/api/place/autocomplete/json?input=${ query }${ types }${ language }${ location }${ radius }${ strictbounds }${ offset }&key=${ apiKey }&sessiontoken=${ sessionToken . current }` ; fetch ( url , { signal : abortSignal . current } ) . then ( data => data . json ( ) ) . then ( data => { dispatch ( { type : data . status , payload : { data , } , } ) ; } ) . catch ( ( ) => { if ( abortController . current . signal . aborted ) { abortController . current = new AbortController ( ) ; abortSignal . current = abortController . current . signal ; } } ) ; } , debounceMs ) ; debouncedFn . current ( ) ; } , [ query , debounceMs , apiKey , options . types , options . language , options . location , options . radius , options . strictbounds , options . offset , type , ] ) ; const getPlaceDetails = ( placeId , placeDetailOptions = { } ) => { return new Promise ( resolve => { const fields = placeDetailOptions . fields ? `&fields=${ placeDetailOptions . fields . join ( ',' ) }` : '' ; const region = placeDetailOptions . region ? `&region=${ placeDetailOptions . region }` : '' ; const language = placeDetailOptions . language ? `&language=${ placeDetailOptions . language }` : options . language ? `&language=${ options . language }}` : '' ; const url = `${ cors }https://maps.googleapis.com/maps/api/place/details/json?placeid=${ placeId }${ fields }${ region }${ language }&key=${ apiKey }&sessiontoken=${ sessionToken . current }` ; fetch ( url , { signal : placesAbortSignal . current } ) . then ( data => data . json ( ) ) . then ( data => { resetSessionToken ( ) ; resolve ( data ) ; } ) . catch ( ( ) => { } ) ; } ) ; } ; const resetSessionToken = ( ) => { sessionToken . current = uuid4 ( ) ; } ; const cancelQuery = ( prediction ) => { if ( abortController . current ) abortController . current . abort ( ) ; dispatch ( { type : 'OK' , payload : { data : { predictions : [ prediction ] , } , } , } ) ; } ; return { results : state . results , isLoading : state . isLoading , error : state . error , getPlaceDetails , cancelQuery , } ; } const reducer = ( state , action ) => { switch ( action . type ) { case 'LOADING' : return { ... state , isLoading : true , } ; case 'OK' : return { ... state , results : action . payload . data , isLoading : false , error : null , } ; case 'ZERO_RESULTS' : return { ... state , results : { predictions : [ ] , } , isLoading : false , error : `No results — try another input.` , } ; case 'INVALID_REQUEST' : return { ... state , isLoading : false , error : null , } ; case 'REQUEST_DENIED' : return { ... state , isLoading : false , error : `Invalid 'key' parameter.` , } ; case 'UNKNOWN_ERROR' : return { ... state , isLoading : false , error : `Unknown error, refresh and try again.` , } ; default : return state ; } } ; function debounce ( func , wait , immediate ? ) { let timeout ; const executedFunction = function ( this ) { let context = this ; let args = arguments ; let later = function ( ) { timeout = null ; if ( ! immediate ) func . apply ( context , args ) ; } ; let callNow = immediate && ! timeout ; clearTimeout ( timeout ) ; timeout = setTimeout ( later , wait ) ; if ( callNow ) func . apply ( context , args ) ; } ; executedFunction . clear = function ( ) { clearTimeout ( timeout ) ; timeout = null ; } ; return executedFunction ; }