Skip to content

Common Offline Error, Need an errorable component pattern #948

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
franklinharvey opened this issue Dec 12, 2018 · 1 comment
Open

Common Offline Error, Need an errorable component pattern #948

franklinharvey opened this issue Dec 12, 2018 · 1 comment

Comments

@franklinharvey
Copy link

franklinharvey commented Dec 12, 2018

Here's a couple StackOverflow questions that hit on this:

I am attracted to this solution that wraps a lot of the code in a component, so that if it fails a React ErrorBoundary can be triggered. However, I'm having a hard time refactoring this in TypeScript due to a lack of React-Google-Maps docs for TS. Plus, I'm not sure the solution's GoogleMapsWrapper would fail in a way that would trigger an ErrorBoundary.

Here's the code I have, mostly boilerplate:

import { GoogleMap, withGoogleMap, Marker, Polyline, Polygon } from 'react-google-maps'

interface IProps {
    center: google.maps.LatLng
    points: google.maps.LatLng[]
    location: google.maps.LatLng | null
    target: google.maps.LatLng[] | null
}

const MARKER_LABEL: google.maps.MarkerLabel = {
    color: '#fff',
    fontSize: '14px',
    text: 'Sample Marker Text'
}

// some more consts....

const MapComponent = (props: IProps): JSX.Element => (
    <GoogleMap center={props.center} defaultMapTypeId={google.maps.MapTypeId.HYBRID} zoom={22} options={OPTIONS}>
        <ZoomLayer maxZoom={22} />
        <Polyline options={POLYLINE_OPTIONS} path={props.points} />
        {props.target !== null && props.location !== null ? <Polyline options={POLYLINE_TO_CENTER_OPTIONS} path={[props.location, averagePoint(props.target)]} /> : null}
        <Marker label={MARKER_LABEL} position={props.location || props.center} icon={MARKER_ICON} />
        {props.target !== null && props.target.length === 1 ? <Marker position={props.center} icon={MARKER_TARGET_ICON} /> : null}
        {props.target !== null && props.target.length > 1 ? <Polygon path={props.target} options={POLYGON_TARGET_OPTIONS} /> : null}
    </GoogleMap>
)

const Map = compose<IProps, IProps>(
    withProps({
        loadingElement: <div className='loading' />,
        containerElement: <div className='map' />,
        mapElement: <div />
    }),
    withGoogleMap
)(MapComponent)

export default Map

Regardless, a lot of example code uses Recompose due to the necessary HOC's, but by building components this way they don't fail in a way that allows React's componentDidCatch to get triggered. Instead you get errors like Uncaught ReferenceError: google is not defined which do not allow any component to render when a page goes offline.

What is a better pattern?

@franklinharvey
Copy link
Author

franklinharvey commented Dec 12, 2018

Here's what I'm thinking as a pattern:

Map.tsx

import * as React from 'react'
import MapComponent from './MapComponent'

interface IProps {
    center: google.maps.LatLng
    points: google.maps.LatLng[]
    location: google.maps.LatLng | null
    target: google.maps.LatLng[] | null
}

class Map extends React.Component<IProps> {

    private OPTIONS: google.maps.MapOptions = {
        backgroundColor: '#111',
        disableDefaultUI: true,
        gestureHandling: 'greedy',
        mapTypeControl: true,
        mapTypeControlOptions: {
            position: google.maps.ControlPosition.BOTTOM_LEFT,
            style: google.maps.MapTypeControlStyle.DEFAULT
        },
        minZoom: 10,
        maxZoom: 22,
        zoomControl: true
    }

    componentDidMount() {
        console.log('Mounted @ ' + Date.now());
    }

  render() {
    return (
        <MapComponent
            googleMapURL="<YOUR API STRING>"
            loadingElement = {<div className='loading' />}
            containerElement = {<div className='map' />}
            mapElement= {<div />}
            OPTIONS = {this.OPTIONS}
            // more if you got them
            center={this.props.center}
            points={this.props.points}
            location = {this.props.location}
            target = {this.props.target}>
        </MapComponent>
    )
  }
}

export default Map

MapComponent.tsx

import * as React from 'react'
import { GoogleMap, withGoogleMap, Marker, Polyline, Polygon, withScriptjs } from 'react-google-maps'
import ZoomLayer from './ZoomLayer'

interface IProps {
    googleMapURL: String
    loadingElement: JSX.Element
    containerElement:  JSX.Element
    mapElement: JSX.Element
    OPTIONS: google.maps.MapOptions
    MARKER_LABEL: google.maps.MarkerLabel
    MARKER_ICON: google.maps.Symbol
    MARKER_TARGET_ICON: google.maps.Symbol
    POLYLINE_OPTIONS: google.maps.PolylineOptions
    POLYLINE_TO_CENTER_OPTIONS: google.maps.PolylineOptions
    POLYGON_TARGET_OPTIONS: google.maps.PolygonOptions
    center: google.maps.LatLng
    points: google.maps.LatLng[]
    location: google.maps.LatLng | null
    target: google.maps.LatLng[] | null
}

const MapComponent = (withScriptjs(withGoogleMap((props: IProps) => {
    function averagePoint(points: google.maps.LatLng[]): google.maps.LatLng {
        let lat: number = points[0].lat()
        let lng: number = points[0].lng()
        for (let i = 1; i < points.length; i++) {
            lat += points[i].lat()
            lng += points[i].lng()
        }

        return new google.maps.LatLng(lat / points.length, lng / points.length)
    }
    return (
        <GoogleMap center={props.center} defaultMapTypeId={google.maps.MapTypeId.HYBRID} zoom={22} options={props.OPTIONS}>
            <ZoomLayer maxZoom={22} />
            <Polyline options={props.POLYLINE_OPTIONS} path={props.points} />
            {props.target !== null && props.location !== null ? <Polyline options={props.POLYLINE_TO_CENTER_OPTIONS} path={[props.location, averagePoint(props.target)]} /> : null}
            <Marker label={props.MARKER_LABEL} position={props.location || props.center} icon={props.MARKER_ICON} />
            {props.target !== null && props.target.length === 1 ? <Marker position={props.center} icon={props.MARKER_TARGET_ICON} /> : null}
            {props.target !== null && props.target.length > 1 ? <Polygon path={props.target} options={props.POLYGON_TARGET_OPTIONS} /> : null}
        </GoogleMap>
    )
})));

export default MapComponent

However

This is only slightly better. Map can fail in a nice way, but MapComponent cannot. Using Recompose doesn't help, it builds exactly the same thing: const MapComponent: React.ComponentClass<IProps & WithGoogleMapProps & WithScriptjsProps, React.ComponentState>

A better MapComponent.tsx

class MapComponent extends React.Component<IProps> {
    averagePoint(points: google.maps.LatLng[]): google.maps.LatLng {
        let lat: number = points[0].lat()
        let lng: number = points[0].lng()
        for (let i = 1; i < points.length; i++) {
            lat += points[i].lat()
            lng += points[i].lng()
        }

        return new google.maps.LatLng(lat / points.length, lng / points.length)
    }
    render() {
        return (
            <withScriptjs>
                <withGoogleMap>
                    <GoogleMap center={props.center} defaultMapTypeId={google.maps.MapTypeId.HYBRID} zoom={22} options={props.OPTIONS}>
                        <ZoomLayer maxZoom={22} />
                        <Polyline options={props.POLYLINE_OPTIONS} path={props.points} />
                        {props.target !== null && props.location !== null ? <Polyline options={props.POLYLINE_TO_CENTER_OPTIONS} path={[props.location, averagePoint(props.target)]} /> : null}
                        <Marker label={props.MARKER_LABEL} position={props.location || props.center} icon={props.MARKER_ICON} />
                        {props.target !== null && props.target.length === 1 ? <Marker position={props.center} icon={props.MARKER_TARGET_ICON} /> : null}
                        {props.target !== null && props.target.length > 1 ? <Polygon path={props.target} options={props.POLYGON_TARGET_OPTIONS} /> : null}
                    </GoogleMap>
                </withGoogleMap>
            </withScriptjs>
    )}
};

But of course you can't do <withScriptjs /> and <withGoogleMap />.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant