2

React/Redux newbie here. I have a form input that allows a user to enter a doctor issue. It returns a list of doctors from the server via Redux action, and displays the doctor list and a marker for each doctor's location on the map (react-google-maps).

When I click submit, the list of doctors for the correct issue displays, the map is there, but with no markers. I can get the markers on the map to display ONLY after submitting the form, THEN clicking on a doctor from the list to display their details.

Want: Enter a doctor issue and render both the list of doctors and markers on the map when the user clicks submit. Then, select a doctor to see their details page (that's another question, routing to dynamic a detail page).

I think, I need to use a life-cycle method but not sure how to implement it. Or, is there a better way to handle this with Redux?

Doctor component:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

import DoctorSearchForm from '../../containers/doctors/DoctorSearchForm';
import DoctorList from './DoctorList';
import Map from '../maps/Map';


class Doctors extends Component {

  constructor(props) {
    super(props);
    this.state = {
      markers: [],
      isMarkerShown: false
    }
  }

  componentDidMount() {
    this.getMarkers();
  }

  getMarkers = () => {
    let practices = this.props.doctors.map(function(doctor, index) {
      return {
        title: doctor.profile.first_name + ' ' + doctor.profile.last_name,
        location: {
          lat: doctor.practices[0].visit_address.lat,
          lng: doctor.practices[0].visit_address.lon
        }
      }
    });
    this.setState({ markers: practices, isMarkerShown: true });
  }

  render() {
    const { doctors, match } = this.props;

    return (
      <div>
        <DoctorSearchForm getMarkers={this.getMarkers} />
        <div className="row">
          <div className="col-md-4">
            <DoctorList doctors={doctors} match={match} />
          </div>
          <div className="col-md-8">
            <Map
              isMarkerShown={this.state.isMarkerShown}
              center={{ lat: 45.6318,lng: -122.6716 }}
              zoom={12}
              markers={this.state.markers}
              />
          </div>
        </div>
      </div>
    );
  }
}

Doctors.propTypes = {
  doctors: PropTypes.array.isRequired,
  match: PropTypes.object.isRequired
}

export default Doctors;

DoctorList component:

import React from "react";
import { Route } from 'react-router-dom';

import DoctorItem from './DoctorItem';
import DoctorView from './DoctorView';


class DoctorList extends React.Component {
  render() {
    const { doctors, match } = this.props;
    const linkList = doctors.map((doctor, index) => {
      return (
        <DoctorItem doctor={doctor} match={match} key={index} />
      );
    });

    return (
      <div>
        <h3>DoctorList</h3>
        <ul>{linkList}</ul>
        <Route path={`${match.url}/:name`}
          render={ (props) => <DoctorView data= {this.props.doctors} {...props} />}
          />
        </div>
      );
  }
}

export default DoctorList;

DoctorItem component:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link, Route } from 'react-router-dom';

import DoctorView from './DoctorView';


const DoctorItem = (props) => {
  const { doctor, match } = props;
    return (
      <li>
        <Link to={{ pathname: `${match.url}/${doctor.profile.first_name}-${doctor.profile.last_name}` }}>
          {doctor.profile.first_name} {doctor.profile.last_name}
        </Link>
      </li>
    )
  }

  DoctorItem.propTypes = {
    doctor: PropTypes.object.isRequired,
  };

  export default DoctorItem;

DoctorView component:

import React from 'react';


const DoctorView = ({ data, match }) => {
  const doctor = data.find(p => `${p.profile.first_name}-${p.profile.last_name}` === match.params.name);


  let doctorData;

  if (doctor) {
    const mappedSpecialties = Object.entries(doctor.specialties).map(([index, specialty]) => {
      return <li key={index} id={index}>{specialty.description}</li>;
      });
      doctorData =
      <div>
        <h5><strong>{doctor.profile.first_name} {doctor.profile.last_name}</strong> - {doctor.profile.title}</h5>
        <img src={doctor.profile.image_url} alt={"Dr." + doctor.profile.first_name + " " + doctor.profile.last_name} />
        <ul>{mappedSpecialties}</ul>
        <p>{doctor.profile.bio}</p>
      </div>;
    }
    return (
      <div>
        {doctorData}
      </div>
    )
  }

  export default DoctorView;

Map component:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withScriptjs, withGoogleMap, GoogleMap, Marker } from 'react-google-maps';
import { compose, withProps } from "recompose";


export default Map = compose(
  withProps({
    googleMapURL:
      "https://maps.googleapis.com/maps/api/js?key={MY_KEY}&v=3.exp&libraries=geometry,drawing,places",
    loadingElement: <div style={{ height: `100%` }} />,
    containerElement: <div style={{ height: `400px` }} />,
    mapElement: <div style={{ height: `100%` }} />
  }),
  withScriptjs,
  withGoogleMap
)(props => (
  <GoogleMap defaultZoom={9} defaultCenter={{ lat: 45.6318,lng: -122.6716 }}>
    {props.markers.map((doctor, index) => {
          const marker = {
            position: { lat: doctor.location.lat, lng: doctor.location.lng },
            title: doctor.title
          }
          return <Marker key={index} {...marker} />;
        })}
  </GoogleMap>
));

I've spent several days trying and searching for answers but no luck. Any help would be greatly appreciated!

HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
danlauby
  • 45
  • 1
  • 6

1 Answers1

1

Just like you calculate the markers when the component mounts, you need to recalculate your markers when you receive new props:

componentWillReceiveProps(nextProps) {
   this.getMarkers(nextProps);
}

This will require you to change your getMarkers signature a bit so that it can accept an argument and use that instead of this.props in your map operation:

getMarkers = (props = this.props) => {
  let practices = props.doctors.map(function(doctor, index) {
    return {
      title: doctor.profile.first_name + ' ' + doctor.profile.last_name,
      location: {
        lat: doctor.practices[0].visit_address.lat,
        lng: doctor.practices[0].visit_address.lon
      }
    }
  });
  this.setState({ markers: practices, isMarkerShown: true });
}

Assuming you are calling getMarkers() in your DoctorSearchForm component, you can remove that since it will automatically update the markers when receiving new props -- or you could bypass state altogether and just calculate it on the fly in render based on the incoming props.

Rob M.
  • 35,491
  • 6
  • 51
  • 50
  • Thank you Rob, it worked! So if I understand correctly componentWillReceiveProps method will listen for a change (comparing current props with new props) in getMarkers, then replace the props in getMarkers function if it sees a difference? – danlauby Feb 06 '18 at 04:57
  • Glad to help! When defined, `componentWillReceiveProps` is automatically called anytime the component receives new props from the parent. With this setup, `getMarkers` will be called with the new props (and updated `doctors` list), which will call `setState` in the `Doctors` component, triggering a rerender with the up-to-date list and map. There was likely a race condition between setting state and receiving props which caused `this.state.markers` to be one step behind. You will find that when using redux, managing state in components makes less sense (and more confusion) – Rob M. Feb 06 '18 at 05:03
  • Great explanation, it really makes sense now. Thanks again! – danlauby Feb 06 '18 at 05:10