1

I am facing such problem, i got my array of records fetched from an API, mapped it into single elements and outputting them as single components. I have function which changes state of parent Component, passes value to child component and child component should hide/show div content after button is clicked.

Of course. It is working, but partially - my all divs are being hidden/shown. I have set specific key to each child component but it doesn't work.

App.js

import React, { Component } from 'react';
import './App.css';

import axios from 'axios';

import countries from '../../countriesList';
import CitySearchForm from './CitySearchForm/CitySearchForm';
import CityOutput from './CityOutput/CityOutput';
import ErrorMessage from './ErrorMessage/ErrorMessage';

class App extends Component {
 state = {
  country: '',
  error: false,
  cities: [],
  infoMessage: '',
  visible: false
 }

getCities = (e) => {
 e.preventDefault();

const countryName = e.target.elements.country.value.charAt(0).toUpperCase() + e.target.elements.country.value.slice(1);

const countryUrl = 'https://api.openaq.org/v1/countries';
const wikiUrl ='https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&format=json&category=city&redirects&origin=*&titles=';
const allowedCountries = new RegExp(/spain|germany|poland|france/, 'i');

if (allowedCountries.test(countryName)) {
  axios
  .get(countryUrl)
  .then( response => {
    const country = response.data.results.find(el => el.name === countryName);
    return axios.get(`https://api.openaq.org/v1/cities?country=${country.code}&order_by=count&sort=desc&limit=10`)
  })
  .then( response => {
    const cities = response.data.results.map(record => {
      return { name: record.city };
    });
    cities.forEach(city => {
      axios
      .get(wikiUrl + city.name)
      .then( response => {
        let id;
        for (let key in response.data.query.pages) {
          id = key;
        }
        const description = response.data.query.pages[id].extract;
        this.setState(prevState => ({
          cities: [...prevState.cities, {city: `${city.name}`, description}],
          infoMessage: prevState.infoMessage = ''
        }))
      })
    })
  })
  .catch(error => { 
    console.log('oopsie, something went wrong', error)
  })
} else {
  this.setState(prevState => ({
    infoMessage: prevState.infoMessage = 'This is demo version of our application and is working only for Spain, Poland, Germany and France',
    cities: [...prevState.cities = []]
  }))
 }
}
descriptionTogglerHandler = () => {
 this.setState((prevState) => {
  return {  visible: !prevState.visible};
 });
};

render () {
return (
  <div className="App">
    <ErrorMessage error={this.state.infoMessage}/>
    <div className="form-wrapper">
      <CitySearchForm getCities={this.getCities} getInformation={this.getInformation} countries={countries}/>
    </div>
    {this.state.cities.map(({ city, description }) => (
      <CityOutput
      key={city} 
      city={city}
      description={description}
      show={this.state.visible}
      descriptionToggler={this.descriptionTogglerHandler} />
    ))}
  </div>
  );
 }
}

export default App;

CityOutput.js

import React, { Component } from 'react';

import './CityOutput.css';


class CityOutput extends Component {
 render() {
  const { city, descriptionToggler, description, show } = this.props;
  let descriptionClasses = 'output-record description'
  if (show) {
   descriptionClasses = 'output-record description open';
  }

 return (
  <div className="output">
    <div className="output-record"><b>City:</b> {city}</div>
    <button onClick={descriptionToggler}>Read more</button>
    <div className={descriptionClasses}>{description}</div>
  </div>
  )
 }
};

export default CityOutput;
1
  • common mistake - one value for an array Commented May 15, 2019 at 18:22

2 Answers 2

2

Put the visible key and the toggle function in the CityOutput instead of having it in the parent

import React, { Component } from "react";

import "./CityOutput.css";

class CityOutput extends Component {
  state = {
    visible: true
  };

  descriptionTogglerHandler = () => {
    this.setState({ visible: !this.state.visible });
  };

  render() {
    const { city, description } = this.props;
    let descriptionClasses = "output-record description";
    if (this.state.visible) {
      descriptionClasses = "output-record description open";
    }

    return (
      <div className="output">
        <div className="output-record">
          <b>City:</b> {city}
        </div>
        <button onClick={() => this.descriptionTogglerHandler()}>Read more</button>
        <div className={descriptionClasses}>{description}</div>
      </div>
    );
  }
}

export default CityOutput;
Sign up to request clarification or add additional context in comments.

3 Comments

shouldn't onClick={() => this.descriptionToggler()} be onClick={this. descriptionTogglerHandler} in your example? Looks like you've got different names for the function
@Ted I've had issues using onClick={this.descriptionTogglerHandler} as the function descriptionTogglerHandler gets called immidiately, didn't test it for this specific example though
That shouldn't be an issue here--but I meant more an issue of the mismatched names: descriptionTogglerHandler vs this.descriptionToggler
1

There are two ways of how I would approach this,

The first one is setting in your state a key property and check and compare that key with the child keys like:

 state = {
  country: '',
  error: false,
  cities: [],
  infoMessage: '',
  visible: false.
currKey: 0
 }
descriptionTogglerHandler = (key) => {
 this.setState((prevState) => {
  return {  currKey: key, visible: !prevState.visible};
 });
};
// then in your child component 

class CityOutput extends Component {
 render() {
  const { city, descriptionToggler, description, show, currKey, elKey } = this.props;
  let descriptionClasses = 'output-record description'
  if (show && elKey === currKey) {
   descriptionClasses = 'output-record description open';
  }

 return (
  <div className="output">
    <div className="output-record"><b>City:</b> {city}</div>
    <button onClick={() => descriptionToggler(elKey)}>Read more</button>
    <div className={descriptionClasses}>{description}</div>
  </div>
  )
 }
};


The other way is to set an isolated state for every child component

class CityOutput extends Component {
constructor(props) {
 this.state = {
   show: false
}
}

function descriptionToggler() {
const {show} = this.state;
this.setState({
show: !show
})
}
 render() {
  const { city, descriptionToggler, description } = this.props;
  let descriptionClasses = 'output-record description'
  if (this.state.show) {
   descriptionClasses = 'output-record description open';
  }

 return (
  <div className="output">
    <div className="output-record"><b>City:</b> {city}</div>
    <button onClick={descriptionToggler}>Read more</button>
    <div className={descriptionClasses}>{description}</div>
  </div>
  )
 }
};

I hope this helps ;)

1 Comment

I very appreciate this answer, since it covers 2 scenarios

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.