Search code examples
reactjsreact-map-gl

React Component Doesn't Update After First Button Click


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>
         )
    }

Solution

  • 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
        }
      }