4

My code generates an input field that allows a user to enter a value to search for. Then when they click the Submit button, it causes displayMap to be true, so that when the MapDisplay component renders, it will trigger an API search via the Map component and return values that are then displayed on the map.

The problem is that this process only works once. When I click the button again, it does do something, I confirmed that it is getting the new value in the input box, but I can't seem to figure out how to get the map to be rendered again.

I've tried setting other variables in the this.setState to try to get it to know that it needs to render the component again, but I guess I'm missing something, because nothing works.

I'm fairly new to React, so any help you can offer would be greatly appreciated.

This is the MainSearchBar.js, where most of the work as described above is happening:

import Map from './newMap.js';

function MapDisplay(props) {
  if (props.displayMap) {
    return <Map toSearch = {props.searchTerm}></Map>;
  } else {
    return "";
  }
}

class MainSearchBar extends React.Component {

    constructor(props) {
      super(props);
      this.state = {
        displayMap: false,
        value: '',
        searchTerm: '',
        isOpened: false
      };
      //this.handleClick = this.handleClick.bind(this);
      this.handleChange = this.handleChange.bind(this);
    }

    handleClick = () => {
        this.setState({

          displayMap: true,
          isOpened: !this.state.isOpened,
          searchTerm: this.state.value
          });
        console.log(this.state.value);
      }

    handleChange(event) {
      this.setState({value: event.target.value});

    }

    render() {
      const displayMap = this.state.displayMap;
         return (
            <div class="homepage-search-bar">
              <input 
                type="text" name="search" value={this.state.value} onChange={this.handleChange} className="main-search-bar" placeholder="Search hashtags">
              </input>
              <button onClick={this.handleClick}>Search</button>
              <MapDisplay displayMap={displayMap} searchTerm={this.state.value} />  
            </div>
         )
    }
}

export default MainSearchBar;

This is where MainSearchBar is being called from

import Top20Box from '../components/getTop20Comp2.js';
import Header from '../components/Header.js';
import MainIntro from '../components/MainIntro.js';
import MainSearchBar from '../components/MainSearchBar.js';
import MainCTA from '../components/MainCTA.js';
import Footer from '../components/Footer.js';

export default class Home extends Component { 
  state = { 
  }

  render () {                                   
      return (
        <React.Fragment>
              <Header>
              </Header>
              <MainIntro />
              <MainSearchBar />
              <div className="top20-text">
                Top 20 trending hashtags
              </div>
              <Top20Box />
              <MainCTA />
              <Footer />
         </React.Fragment>
      )
   }
}

And this is the Map component itself, in case you need it:

import React from 'react';
import ReactMapGL, {Marker, Popup} from 'react-map-gl';
import axios from 'axios';

//for the loading animation function
import FadeIn from "react-fade-in";
import Lottie from "react-lottie";
import * as loadingData from "../assets/loading.json";

var locationCoordinates = [];
var locationToSearch = "";
var returnedKeywordSearch = [];
var newArray = [];

const defaultOptions = {
  loop: true,
  autoplay: true,
  animationData: loadingData.default,
  rendererSettings: {
    preserveAspectRatio: "xMidYMid slice"
  }
};

export default class Map extends React.Component {

//sets components for the map, how big the box is and where the map is centered when it opens
  state = {
            viewport: {
              width: "75vw",
              height: "50vh",
              latitude: 40.4168,
              longitude: 3.7038,
              zoom: .5
            },
            tweetSpots: null, //data from the API
            selectedSpot: null,
            done: undefined, //for loading function
          };

  async componentDidMount() {
    //searches the api for the hashtag that the user entered

    await axios.get(`https://laffy.herokuapp.com/search/${this.props.toSearch}`).then(function(response) {
        returnedKeywordSearch = response.data;
      }) //if the api call returns an error, ignore it
      .catch(function(err) {
        return null;
      });

      //goes through the list of locations sent from the api above and finds the latitude/longitude for each
      var count = 0;
      while (count < returnedKeywordSearch.length) {
        locationToSearch = returnedKeywordSearch[count].location;
        if (locationToSearch !== undefined) {
          var locationList = await axios.get(`https://api.mapbox.com/geocoding/v5/mapbox.places/${locationToSearch}.json?access_token=pk.eyJ1IjoibGF1bmRyeXNuYWlsIiwiYSI6ImNrODlhem95aDAzNGkzZmw5Z2lhcjIxY2UifQ.Aw4J8uxMSY2h4K9qVJp4lg`)
          .catch(function(err) {
            return null;
          });

          if (locationList !== null) {
            if (Array.isArray(locationList.data.features) && locationList.data.features.length)  
             {
              locationCoordinates.push(locationList.data.features[0].center);
              if (returnedKeywordSearch[count].location!== null && returnedKeywordSearch[count].location!==""
                  && locationList.data.features[0].center !== undefined)
                {newArray.push({
                            id: returnedKeywordSearch[count].id, 
                            createdAt: returnedKeywordSearch[count].createdAt,
                            text: returnedKeywordSearch[count].text,
                            name: returnedKeywordSearch[count].name,
                            location: returnedKeywordSearch[count].location,
                            coordinates: locationList.data.features[0].center
                });
                }
            } 
          }
        }

        count++;
      }
      this.setState({tweetSpots: newArray});
      this.setState({ done: true}); //sets done to true so that loading animation goes away and map displays
  }     
//is triggered when a marker on the map is hovered over
  setSelectedSpot = object => {
    this.setState({
     selectedSpot: object
    });
  };

//creates markers that display on the map, using location latitude and longitude
  loadMarkers = () => {
    return this.state.tweetSpots.map((item,index) => {
      return (
        <Marker
          key={index}
          latitude={item.coordinates[1]}
          longitude={item.coordinates[0]}
        >
          <img class="mapMarker"
            onMouseOver={() => {
              this.setSelectedSpot(item);
            }}
            src="/images/yellow7_dot.png" alt="" />
        </Marker>
      );
    });
  };

//closes popup when close is clicked
  closePopup = () => {
    this.setState({
      selectedSpot: null
    }); 
  };

 //renders map component and loading animation
  render() {
    return (
      <div className="App">
        <div className="map">
          {!this.state.done ? (
          <FadeIn>
            <div class="d-flex justify-content-center align-items-center">
              <Lottie options={defaultOptions} width={400} />
            </div>
          </FadeIn>
        ) : (
        <ReactMapGL  {...this.state.viewport} mapStyle="mapbox://styles/mapbox/outdoors-v11"
         onViewportChange={(viewport => this.setState({viewport}))} 
         mapboxApiAccessToken="pk.eyJ1IjoibGF1bmRyeXNuYWlsIiwiYSI6ImNrODlhem95aDAzNGkzZmw5Z2lhcjIxY2UifQ.Aw4J8uxMSY2h4K9qVJp4lg">

          {this.loadMarkers()}

          {this.state.selectedSpot !== null ? (
            <Popup
              key={this.state.selectedSpot.id}
              tipSize={5}
              latitude={this.state.selectedSpot.coordinates[1]}
              longitude={this.state.selectedSpot.coordinates[0]}
              closeButton={true}
              closeOnClick={false}
              onClose={this.closePopup}
            >
               <div className="mapPopup">
                 <div className="header"> Tweet </div>
                 <div className="content">
                   {" "}
                 <p>
                   <b>Name:</b> {this.state.selectedSpot.name}
                 </p>
                 <p>
                   <b>Tweet:</b> {this.state.selectedSpot.text}</p>
                   <p><a href={'https://www.twitter.com/user/status/' + this.state.selectedSpot.id}target="_blank" rel="noopener noreferrer">View Tweet in Twitter</a>
                  </p>
                 </div>

               </div>  
            </Popup>
          ) : null}

        </ReactMapGL>

        )}
        </div>
      </div>

      );
  }
}

Update: 4/28, per the answer I received, I update the render of the MainSearchBar.js to look like this:

render() {
      const displayMap = this.state.displayMap;
         return (
            <div class="homepage-search-bar">
              <input 
                type="text" name="search" value={this.state.value} onChange={this.handleChange} className="main-search-bar" placeholder="Search hashtags">
              </input>
              <button onClick={this.handleClick}>Search</button>

              {this.state.displayMap && <Map toSearch = {this.searchTerm}></Map>}


            </div>
         )
    }
Nova
  • 69
  • 1
  • 8

2 Answers2

3

When you click the button again, the state of MainSearchBar.js updates but the functional component MapDisplay does not and thus the Map does not update as well.

There are many ways to resolve this. Looking at the code, it looks like MapDisplay doesn't do much so you can consider replacing it with conditional rendering.

MainSearchBar.js

    render() {
      const displayMap = this.state.displayMap;
         return (
            <div class="homepage-search-bar">
              <input 
                type="text" name="search" value={this.state.value} onChange={this.handleChange} className="main-search-bar" placeholder="Search hashtags">
              </input>
              <button onClick={this.handleClick}>Search</button>
              {this.state.displayMap && <Map toSearch = {props.searchTerm}></Map>}
            </div>
         )
    }

Then in your Map component, add a componentDidUpdate lifecycle method to detect updates to the prop which does the same thing as componentDidMount when the props are updated.

  async componentDidMount(prevProps) {
    if (props.toSearch != prevProps.toSearch) {
      await axios.get(`https://laffy.herokuapp.com/search/${this.props.toSearch}`).then(function(response) {
        returnedKeywordSearch = response.data;
      }) //if the api call returns an error, ignore it
      .catch(function(err) {
        return null;
      });

      //goes through the list of locations sent from the api above and finds the latitude/longitude for each
      var count = 0;
      while (count < returnedKeywordSearch.length) {
        locationToSearch = returnedKeywordSearch[count].location;
        if (locationToSearch !== undefined) {
          var locationList = await axios.get(`https://api.mapbox.com/geocoding/v5/mapbox.places/${locationToSearch}.json?access_token=pk.eyJ1IjoibGF1bmRyeXNuYWlsIiwiYSI6ImNrODlhem95aDAzNGkzZmw5Z2lhcjIxY2UifQ.Aw4J8uxMSY2h4K9qVJp4lg`)
          .catch(function(err) {
            return null;
          });

          if (locationList !== null) {
            if (Array.isArray(locationList.data.features) && locationList.data.features.length)  
             {
              locationCoordinates.push(locationList.data.features[0].center);
              if (returnedKeywordSearch[count].location!== null && returnedKeywordSearch[count].location!==""
                  && locationList.data.features[0].center !== undefined)
                {newArray.push({
                            id: returnedKeywordSearch[count].id, 
                            createdAt: returnedKeywordSearch[count].createdAt,
                            text: returnedKeywordSearch[count].text,
                            name: returnedKeywordSearch[count].name,
                            location: returnedKeywordSearch[count].location,
                            coordinates: locationList.data.features[0].center
                });
                }
            } 
          }
        }

        count++;
      }
      this.setState({tweetSpots: newArray});
      this.setState({ done: true}); //sets done to true so that loading animation goes away and map displays
    }
  }    
wxker
  • 1,841
  • 1
  • 6
  • 16
  • Thank you for your quick response! I changed render in MainSearchBar.js as you instructed. See edit above. But I'm not able to get ComponentDidUpdate in the Map component set up correctly. I either get an endless loop or the same problem as before.Could you give me some direction on what that should look like? – Nova Apr 28 '20 at 23:17
  • @Nova Updated. As mentioned, it is exactly the same as your `componentDidMount` with just an additional if condition – wxker Apr 29 '20 at 00:50
0

@wxker Thanks for all your help! You certainly got me pointed in the right direction.

I changed render in MainSearchBar.js back to what it was originally.

And I added a ComponentDidUpdate to the Map component, as follows below:

async componentDidUpdate(prevProps) {

    //searches the api for the hashtag that the user entered
    if (this.props.toSearch !== prevProps.toSearch) {

and then the rest was the same as the original componentDidMount.

Nova
  • 69
  • 1
  • 8
  • Resurrection: I've been using useEffect rather than the traditional lifecycle hooks lately, you might find it to be simpler. – Brad Fellows Oct 15 '20 at 01:43
  • Thanks, @BradFellows! Yep, I have definitely moved on to useEffect since then. – Nova Dec 22 '20 at 23:17