2

I first make an Ajax call (to an API) which provides me some data, a list of achievements (array of objects). I would like to loop through this array, show the first achievement as a Modal and on click of a button close the modal then show the next one (next achievement) and so on.

Ajax call providing the data:

getAchievements = () => {
    fetch(url + '/achievements', {
      method: 'get',
      headers: {
        Accept: 'application/json',
        'Content-type': 'application/json'
      }
    })
    .then((data) => data.json())
    .then((data) => {
      this.props.addData({
        achievements: data.achievements
      })

      if(this.props.store.achievements.length > 0) {
        this.setState({
          showAchievementModal: true
        })
      }
    })
    .catch((error) => {
      console.error(error)
    })
}

Here I show the modals:

render() {
    return (
        {this.state.showAchievementModal &&
          <Modal
            animationType={'fade'}
            visible={this.props.store.isModalAchievementVisible}
            >
            {this.props.store.achievements.map((data,index)=>{
                return(
                    <View key={index}>
                        <View style={styles.container}>
                            <Text>{data.title}</Text>
                            <Text>{data.description}</Text>

                            <TouchableOpacity onPress={this.closeModal}>
                                <Text>Collect</Text>
                            </TouchableOpacity>
                        </View>
                  </View>
                )
            })}
          </Modal>
        }
    )
}

At the moment all the Modals open at the same time. How could I open them one after the other after clicking the Collect button?

John
  • 3,529
  • 14
  • 42
  • 48

2 Answers2

2

This is the updated version of my code that works:

Initialising activeModalIndex in the constructor:

constructor(props) {
  super(props)

  this.state = {
    activeModalIndex: 0
  }
}

Get achievements:

getAchievements = () => {
  if(this.props.store.achievements.length > 0) {
    this.setState({
      showAchievementModal: true,
      activeModalIndex: 0,
    })
  }
}

Render function:

 render() {
   return this.props.store.achievements.map((data,index) => this.state.activeModalIndex === index &&
     <Modal>
       <View key={index}>
         <View style={styles.container}>
           <Text>{data.title}</Text>
           <Text>{data.description}</Text>

           <TouchableOpacity onPress={this.closeModal}>
             <Text>Collect</Text>
           </TouchableOpacity>
        </View>
      </View>
    </Modal>
  )
}

Close Modal:

closeModal = () => {
  const maxIndex = this.props.store.achievements.length - 1
  const currentIndex = this.state.activeModalIndex
  const isLastModal = currentIndex === maxIndex
  const newIndex = isLastModal? -1: currentIndex +1

  this.setState({
    activeModalIndex: newIndex
  })
}
John
  • 3,529
  • 14
  • 42
  • 48
1

The problem is that you have multiple Modals on your page and they all use the same boolean to check if they should be rendered. Initially, showAchievementModal is set to true, so all modals are rendered. Furthermore, after you set showAchievementModal to false in closeModal, it will permanently stay false, so no additional modals will get rendered.

render() {
    return (
        {this.state.showAchievementModal &&
          <Modal
             ...          
          </Modal>
        }
    )
}

Instead of showAchievementModal you should be keeping track of index of active modal. So, after you fetch the list of achievements from your API, set the activeModalIndex to 0. After the user dismisses this first modal, set the activeModalIndex to 1 inside the closeModal method, then set it to 2 after the second modal is closed and so on.

Now, for every modal to correspond to a single achievement, we must map each element of the achievements array to a single Modal and conditionally render it only if its corresponding index is the active one.

render() {
    const achievements = this.props.store.achievements;
    const { activeModalIndex } = this.state;

    return achievements.map((data, index) => activeModalIndex === index &&
      <Modal key={index}>
        <View>
          <View style={styles.container}>
            <Text>{data.title}</Text>
            <Text>{data.description}</Text>

            <TouchableOpacity onPress={this.closeModal}>
              <Text>Collect</Text>
            </TouchableOpacity>
          </View>
        </View>
      </Modal>
    )
}

When the users dismisses the currently active modal, simply increment the index of the active modal and the next modal will appear instead of the current one. If the new incremented value is equal or larger than the array length, nothing will get rendered, so no need to check for max index value before setting new state.

closeModal = () => {   
    this.setState(previousState => ({
      activeModalIndex: previousState.activeModalIndex + 1,
    }))
}

Also, please read about the dangers of setting index as key when rendering lists. If you happen to need ordering achievements by some value/priority and users can retrieve multiple pages of their achievements, it might cause rendering wrong components.

ToneCrate
  • 524
  • 6
  • 12
  • Thanks for your response! Worked like a charm. I added an updated version of my code for others – John Mar 10 '19 at 18:57
  • Glad I could help. Also, I've updated my answer: I've moved the key prop to the Modal component and also linked an article about dangers of using the mapping index as key – ToneCrate Mar 10 '19 at 23:57
  • One more thing. When closing the last modal (last achievement from the array) - how would you reset activeModalIndex to 0 ? – John Mar 11 '19 at 10:05
  • What are you trying to accomplish? Resetting activeModalIndex to 0 after closing the last modal will display the first modal again. If you just need to close the last one, there's no need to reset anything. If you have 3 achievements you will have 3 modals - Modal 0, Modal 1 and Modal 2. Once you close the last one - Modal 2, active index will go from 2 to 3, and since you don't have Modal 3 which checks activeIndex === 3, nothing will get rendered. – ToneCrate Mar 11 '19 at 13:42
  • However, if you need to set a flag when the last modal is closed, you can do closeModal = () => { const maxIndex = this.state.achievements.length - 1; const currentIndex = this.state.activeModalIndex; const lastModalClosed = currentIndex === maxIndex; // set activeModalIndex to an invalid value if the last modal is closed, say, -1 // otherwise, increment it const newIndex = isLastModal ? -1 : currentIndex + 1; this.setState(previousState => ({ activeModalIndex: newIndex, lastModalClosed, })) } – ToneCrate Mar 11 '19 at 13:50
  • To give you a bit more background - after the modals have been displayed based on the array (currently working fine) the idea is to reset activeModalIndex in order to display a new modal if new achievements get pulled from the API – John Mar 11 '19 at 15:44
  • Aha, I get it. Well, the code from my comment above is relevant, although not readable. Here https://www.codepile.net/pile/AwD192kb If **isLastModal** is true, that's the signal for you to check for more achievements and I'd advise resetting activeModalIndex to 0 inside your getAchievements method. Also, now I sure your modals should definitely use some **achievement_id** from your dabatase as key when mapping. Your use case is prone to rendering errors if you use *mapping index* as key. You might render the same achievement multiple times or not render the ones that should be rendered. – ToneCrate Mar 11 '19 at 18:33
  • Thanks a lot for this. Very much appreciated. I also updated my code (second answer) as a reference. It works fine. I now just need to update they key and use achivement_id from the DB but haven't seen any issues so for. – John Mar 12 '19 at 11:30