React-google-maps: Support React16

Created on 1 Oct 2017  路  15Comments  路  Source: tomchentw/react-google-maps

From what I see mainly you need to replace all calls to ReactDOM.unstable_renderSubtreeIntoContainer with calls to ReactDOM.createPortal
It's not a drop in replacement, as createPortal needs to return from the render() function.

PRs welcomed

Most helpful comment

here is a working patch of SearchBox that runs with React 16. (Note, I converted to typescript because of our codebase, but it is not strongly typed and inlined the helpers which you don't need to do).

/* global google */
import canUseDOM from 'can-use-dom';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';

declare const google: any;

function rdcUncontrolledAndControlledProps(acc, value, key) {
  if (_.has(acc.prevProps, key)) {
    const match = key.match(/^default(\S+)/);
    if (match) {
      const unprefixedKey = _.lowerFirst(match[1]);
      if (!_.has(acc.nextProps, unprefixedKey)) {
        acc.nextProps[unprefixedKey] = acc.prevProps[key];
      }
    } else {
      acc.nextProps[key] = acc.prevProps[key];
    }
  }
  return acc;
}

function applyUpdaterToNextProps(updaterMap, prevProps, nextProps, instance) {
  _.forEach(updaterMap, (fn: any, key) => {
    const nextValue = nextProps[key];
    if (nextValue !== prevProps[key]) {
      fn(instance, nextValue);
    }
  });
}

export function construct(propTypes, updaterMap, prevProps, instance) {
  const { nextProps } = _.reduce(propTypes, rdcUncontrolledAndControlledProps, {
    nextProps: {},
    prevProps,
  });
  applyUpdaterToNextProps(
    updaterMap,
    {
      /* empty prevProps for construct */
    },
    nextProps,
    instance
  );
}

export function componentDidMount(component, instance, eventMap) {
  registerEvents(component, instance, eventMap);
}

export function componentDidUpdate(
  component,
  instance,
  eventMap,
  updaterMap,
  prevProps
) {
  component.unregisterAllEvents();
  applyUpdaterToNextProps(updaterMap, prevProps, component.props, instance);
  registerEvents(component, instance, eventMap);
}

export function componentWillUnmount(component) {
  component.unregisterAllEvents();
}

function registerEvents(component, instance, eventMap) {
  const registeredList = _.reduce(
    eventMap,
    (acc: any[], googleEventName, onEventName) => {
      if (_.isFunction(component.props[onEventName])) {
        acc.push(
          google.maps.event.addListener(
            instance,
            googleEventName,
            component.props[onEventName]
          )
        );
      }
      return acc;
    },
    []
  );

  component.unregisterAllEvents = _.bind(
    _.forEach,
    null,
    registeredList,
    unregisterEvent
  );
}

function unregisterEvent(registered) {
  google.maps.event.removeListener(registered);
}

export const MAP = `__SECRET_MAP_DO_NOT_USE_OR_YOU_WILL_BE_FIRED`;
export const SEARCH_BOX = `__SECRET_SEARCH_BOX_DO_NOT_USE_OR_YOU_WILL_BE_FIRED`;

/**
 * @url https://developers.google.com/maps/documentation/javascript/3.exp/reference#SearchBox
 */
export class SearchBox extends React.PureComponent<any, any> {
  static propTypes = {
    /**
     * Where to put `<SearchBox>` inside a `<GoogleMap>`
     *
     * @example google.maps.ControlPosition.TOP_LEFT
     * @type number
     */
    controlPosition: PropTypes.number,

    /**
     * @type LatLngBounds|LatLngBoundsLiteral
     */
    defaultBounds: PropTypes.any,

    /**
     * @type LatLngBounds|LatLngBoundsLiteral
     */
    bounds: PropTypes.any,

    /**
     * function
     */
    onPlacesChanged: PropTypes.func,
  };

  static contextTypes = {
    [MAP]: PropTypes.object,
  };

  constructor(props) {
    super(props);

    this.state = {
      [SEARCH_BOX]: null,
    };
  }

  containerElement;
  mountControlIndex;

  componentWillMount() {
    if (!canUseDOM || this.containerElement) {
      return;
    }
    this.containerElement = document.createElement(`div`);
    this.handleRenderChildToContainerElement();
  }

  componentDidMount() {
    /*
     * @url https://developers.google.com/maps/documentation/javascript/3.exp/reference#SearchBox
     */
    const searchBox = new google.maps.places.SearchBox(
      this.containerElement.firstChild
    );
    construct(SearchBox.propTypes, updaterMap, this.props, searchBox);
    this.setState({
      [SEARCH_BOX]: searchBox,
    });
    componentDidMount(this, searchBox, eventMap);
    this.handleMountAtControlPosition();
  }

  componentWillUpdate(nextProp) {
    if (this.props.controlPosition !== nextProp.controlPosition) {
      this.handleUnmountAtControlPosition();
    }
  }

  componentDidUpdate(prevProps) {
    if (!this.state[SEARCH_BOX]) {
      return;
    }
    componentDidUpdate(
      this,
      this.state[SEARCH_BOX],
      eventMap,
      updaterMap,
      prevProps
    );
    if (this.props.children !== prevProps.children) {
      this.handleRenderChildToContainerElement();
    }
    if (this.props.controlPosition !== prevProps.controlPosition) {
      this.handleMountAtControlPosition();
    }
  }

  componentWillUnmount() {
    componentWillUnmount(this);
    this.handleUnmountAtControlPosition();
    if (this.containerElement) {
      this.setState({
        showSearchBox: false
      });
      this.containerElement = null;
    }
  }

  handleRenderChildToContainerElement() {
    this.setState({
      showSearchBox: true,
    });
  }

  handleMountAtControlPosition() {
    if (isValidControlPosition(this.props.controlPosition)) {
      this.mountControlIndex = this.context[MAP].controls[this.props.controlPosition].push(
        this.containerElement.firstChild) - 1;
    }
  }

  handleUnmountAtControlPosition() {
    if (isValidControlPosition(this.props.controlPosition)) {
      const child = this.context[MAP].controls[
        this.props.controlPosition
      ].removeAt(this.mountControlIndex);
      this.containerElement.appendChild(child);
    }
  }

  render() {
    if (!this.state.showSearchBox || !this.containerElement) {
      return false;
    }
    return (ReactDOM as any).createPortal(
      this.props.children,
      this.containerElement
    );
  }

  /**
   * Returns the bounds to which query predictions are biased.
   * @type LatLngBounds
   * @public
   */
  getBounds() {
    return this.state[SEARCH_BOX].getBounds();
  }

  /**
   * Returns the query selected by the user, or null if no places have been found yet, to be used with places_changed event.
   * @type Array<PlaceResult>nullplaces_changed
   * @public
   */
  getPlaces() {
    return this.state[SEARCH_BOX].getPlaces();
  }
}

export default SearchBox;

const isValidControlPosition = _.isNumber;

const eventMap = {
  onPlacesChanged: 'places_changed',
};

const updaterMap = {
  bounds(instance, bounds) {
    instance.setBounds(bounds);
  },
};

All 15 comments

I agree. May conduct some API changes as well.

probably use of PropTypes as well...

@bondarewicz aren't we using PropTypes now?

@tomchentw is this package compatible with React16 currently?

@quachsimon dunno. Haven't tried yet.

Zero issues with React 16 and the library, no warnings other than the peer dependency.

Nearly everything is backwards compatible, and we're already using prop-types

I'm testing right now just to be sure as I've been on vacation and on Friday I think everything was pretty good.

I'll put together a PR with some updates if things pan out

@oshalygin There are some components which work as is, probably that is what you are seeing, But SearchBox or anything that uses portal does not work.

Gotcha, yeah I'll have to look a bit deeper today.

here is a working patch of SearchBox that runs with React 16. (Note, I converted to typescript because of our codebase, but it is not strongly typed and inlined the helpers which you don't need to do).

/* global google */
import canUseDOM from 'can-use-dom';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';

declare const google: any;

function rdcUncontrolledAndControlledProps(acc, value, key) {
  if (_.has(acc.prevProps, key)) {
    const match = key.match(/^default(\S+)/);
    if (match) {
      const unprefixedKey = _.lowerFirst(match[1]);
      if (!_.has(acc.nextProps, unprefixedKey)) {
        acc.nextProps[unprefixedKey] = acc.prevProps[key];
      }
    } else {
      acc.nextProps[key] = acc.prevProps[key];
    }
  }
  return acc;
}

function applyUpdaterToNextProps(updaterMap, prevProps, nextProps, instance) {
  _.forEach(updaterMap, (fn: any, key) => {
    const nextValue = nextProps[key];
    if (nextValue !== prevProps[key]) {
      fn(instance, nextValue);
    }
  });
}

export function construct(propTypes, updaterMap, prevProps, instance) {
  const { nextProps } = _.reduce(propTypes, rdcUncontrolledAndControlledProps, {
    nextProps: {},
    prevProps,
  });
  applyUpdaterToNextProps(
    updaterMap,
    {
      /* empty prevProps for construct */
    },
    nextProps,
    instance
  );
}

export function componentDidMount(component, instance, eventMap) {
  registerEvents(component, instance, eventMap);
}

export function componentDidUpdate(
  component,
  instance,
  eventMap,
  updaterMap,
  prevProps
) {
  component.unregisterAllEvents();
  applyUpdaterToNextProps(updaterMap, prevProps, component.props, instance);
  registerEvents(component, instance, eventMap);
}

export function componentWillUnmount(component) {
  component.unregisterAllEvents();
}

function registerEvents(component, instance, eventMap) {
  const registeredList = _.reduce(
    eventMap,
    (acc: any[], googleEventName, onEventName) => {
      if (_.isFunction(component.props[onEventName])) {
        acc.push(
          google.maps.event.addListener(
            instance,
            googleEventName,
            component.props[onEventName]
          )
        );
      }
      return acc;
    },
    []
  );

  component.unregisterAllEvents = _.bind(
    _.forEach,
    null,
    registeredList,
    unregisterEvent
  );
}

function unregisterEvent(registered) {
  google.maps.event.removeListener(registered);
}

export const MAP = `__SECRET_MAP_DO_NOT_USE_OR_YOU_WILL_BE_FIRED`;
export const SEARCH_BOX = `__SECRET_SEARCH_BOX_DO_NOT_USE_OR_YOU_WILL_BE_FIRED`;

/**
 * @url https://developers.google.com/maps/documentation/javascript/3.exp/reference#SearchBox
 */
export class SearchBox extends React.PureComponent<any, any> {
  static propTypes = {
    /**
     * Where to put `<SearchBox>` inside a `<GoogleMap>`
     *
     * @example google.maps.ControlPosition.TOP_LEFT
     * @type number
     */
    controlPosition: PropTypes.number,

    /**
     * @type LatLngBounds|LatLngBoundsLiteral
     */
    defaultBounds: PropTypes.any,

    /**
     * @type LatLngBounds|LatLngBoundsLiteral
     */
    bounds: PropTypes.any,

    /**
     * function
     */
    onPlacesChanged: PropTypes.func,
  };

  static contextTypes = {
    [MAP]: PropTypes.object,
  };

  constructor(props) {
    super(props);

    this.state = {
      [SEARCH_BOX]: null,
    };
  }

  containerElement;
  mountControlIndex;

  componentWillMount() {
    if (!canUseDOM || this.containerElement) {
      return;
    }
    this.containerElement = document.createElement(`div`);
    this.handleRenderChildToContainerElement();
  }

  componentDidMount() {
    /*
     * @url https://developers.google.com/maps/documentation/javascript/3.exp/reference#SearchBox
     */
    const searchBox = new google.maps.places.SearchBox(
      this.containerElement.firstChild
    );
    construct(SearchBox.propTypes, updaterMap, this.props, searchBox);
    this.setState({
      [SEARCH_BOX]: searchBox,
    });
    componentDidMount(this, searchBox, eventMap);
    this.handleMountAtControlPosition();
  }

  componentWillUpdate(nextProp) {
    if (this.props.controlPosition !== nextProp.controlPosition) {
      this.handleUnmountAtControlPosition();
    }
  }

  componentDidUpdate(prevProps) {
    if (!this.state[SEARCH_BOX]) {
      return;
    }
    componentDidUpdate(
      this,
      this.state[SEARCH_BOX],
      eventMap,
      updaterMap,
      prevProps
    );
    if (this.props.children !== prevProps.children) {
      this.handleRenderChildToContainerElement();
    }
    if (this.props.controlPosition !== prevProps.controlPosition) {
      this.handleMountAtControlPosition();
    }
  }

  componentWillUnmount() {
    componentWillUnmount(this);
    this.handleUnmountAtControlPosition();
    if (this.containerElement) {
      this.setState({
        showSearchBox: false
      });
      this.containerElement = null;
    }
  }

  handleRenderChildToContainerElement() {
    this.setState({
      showSearchBox: true,
    });
  }

  handleMountAtControlPosition() {
    if (isValidControlPosition(this.props.controlPosition)) {
      this.mountControlIndex = this.context[MAP].controls[this.props.controlPosition].push(
        this.containerElement.firstChild) - 1;
    }
  }

  handleUnmountAtControlPosition() {
    if (isValidControlPosition(this.props.controlPosition)) {
      const child = this.context[MAP].controls[
        this.props.controlPosition
      ].removeAt(this.mountControlIndex);
      this.containerElement.appendChild(child);
    }
  }

  render() {
    if (!this.state.showSearchBox || !this.containerElement) {
      return false;
    }
    return (ReactDOM as any).createPortal(
      this.props.children,
      this.containerElement
    );
  }

  /**
   * Returns the bounds to which query predictions are biased.
   * @type LatLngBounds
   * @public
   */
  getBounds() {
    return this.state[SEARCH_BOX].getBounds();
  }

  /**
   * Returns the query selected by the user, or null if no places have been found yet, to be used with places_changed event.
   * @type Array<PlaceResult>nullplaces_changed
   * @public
   */
  getPlaces() {
    return this.state[SEARCH_BOX].getPlaces();
  }
}

export default SearchBox;

const isValidControlPosition = _.isNumber;

const eventMap = {
  onPlacesChanged: 'places_changed',
};

const updaterMap = {
  bounds(instance, bounds) {
    instance.setBounds(bounds);
  },
};

SearchBox is not working with React 16.0.0
Error: Expected subtree parent to be a mounted class component.

provide simple patch.

import React from 'react';
import ReactDOM from 'react-dom';
import SearchBox from "react-google-maps/lib/components/places/SearchBox";

let {componentWillMount, componentDidMount, componentWillUnmount, componentDidUpdate}=SearchBox.prototype;
SearchBox.prototype.componentWillMount=function(){
  this._containerElement = document.createElement(`div`);
}
SearchBox.prototype.componentDidMount=function(){
  componentWillMount.call(this);
  setTimeout(()=>{
    /*make sure the state[SEARCH_BOX] exists */
    componentDidMount.call(this);
    this._componentDidMount=true;
  });
}
/*fix eventMap */
SearchBox.prototype.componentDidUpdate=function(prevProps){
  if(this._componentDidMount){
    componentDidUpdate.call(this, prevProps);
  }
}
SearchBox.prototype.handleRenderChildToContainerElement=function(){
  this.containerElement=this._containerElement;
}
SearchBox.prototype.render=function(){
  return ReactDOM.createPortal(
    this.props.children,
    this._containerElement
  );
}
SearchBox.prototype.componentWillUnmount=function(){
  if(this.containerElement){
    componentWillUnmount.call(this);
  }
}

Any fix for the SearchBox that supports React@15 as well?

Released v9.1.0

Was this page helpful?
0 / 5 - 0 ratings

Related issues

madbean picture madbean  路  3Comments

EvHaus picture EvHaus  路  3Comments

farhan687 picture farhan687  路  3Comments

timkraut picture timkraut  路  3Comments

0x1bitcrack3r picture 0x1bitcrack3r  路  3Comments