How to fix Ionic List component flickering

I’m new to Ionic/React programming. I’m currently building an application that is connected to Google Firebase and makes automatic calls to the cloud firestore I have there.

I have a page that renders an Ionic component with two custom Ionic list components. The list has items which when clicked display an alert that prompts the user.

I noticed that whenever I click on the list and the alert opens the Ionic List component flickers when the Ion page refreshes to display the alert. The component also flickers when I close the alert.

Rulesets.tsx

const Rulesets: React.FC = () => {
  const [loading, setLoading] = useState(true)
  const [rulesList, setRulesList] = useState([])
  const [userRulesList, setUserRulesList] = useState([])
  const [downloadRules, setDownloadRules] = useState(false)
  const [deleteRules, setDeleteRules] = useState(false)
  const [focusedRulesetName, setFocusRulesetName] = useState("")
  const [focusedRulesetId, setFocusRulesetId] = useState("")
  const [focusedRulesetVersion, setFocusRulesetVer] = useState("")

  useEffect(() => {
    getUserRulesets().then((rulesets: any) => {
      if(rulesets){
        setUserRulesList(rulesets)
        console.log("Use effect setting user rules list.")
      }
      else{
        console.log("Use effect found no rulesets found for user.")
      }
    }).finally(() => setLoading(false))
  }, [])

  useEffect(() => {
    getRulesets().then((rulesets: any) => {
      if(rulesets){
        setRulesList(rulesets.map((rule: {rule: []}) => rule))
        console.log("Use effect setting available rules list.")
      }
    }).finally(() => setLoading(false))
  }, [])

  function updateUserRulesList(){
    getUserRulesets().then((rulesets: any) => {
      if(rulesets){
        console.log("Current user rules list: ",userRulesList)
        console.log("New user rules list: ", rulesets)
        if(rulesets === userRulesList){
          console.log("Rules unchanged.")
        }
        setUserRulesList(rulesets)
      }
      else{
        console.log("No rulesets found for user.")
        setUserRulesList([])
      }
    })
  }

  function openAlert(check: boolean, alertName: string, rulesetId: string, rulesetName: string, rulesetVer: string){
    setFocusRulesetName(rulesetName)
    setFocusRulesetId(rulesetId)
    setFocusRulesetVer(rulesetVer)
    
    if(alertName === "download"){
      setDownloadRules(check)
    }
    else{
      setDeleteRules(check)
    }
  }

  const RulesetItem = (prop: {id: string, name: string, version: string}) => {
    var installed = false
    var rule: {id: string}

    for(rule of userRulesList){
        if(rule.id === prop.id){
          installed = true
        }
      }
    
    if(installed){
      return (
        <IonItem key={prop.id+ "-available"}>
        <IonLabel>
          <h2>{prop.name}</h2>
          <h3>Version: {prop.version}</h3>
          <p>Installed</p>
        </IonLabel>
      </IonItem>
      )
    }
    return (
      <IonItem key={prop.id+ "-available"} button={true} onClick={() => openAlert(true, "download", prop.id, prop.name, prop.version)}>
      <IonLabel>
        <h2>{prop.name}</h2>
        <h3>Version: {prop.version}</h3>
      </IonLabel>
    </IonItem>
    )    
  }

  const DownloadAlert = () => {
    return (
      <IonAlert
        isOpen={downloadRules}
        onDidDismiss={() => setDownloadRules(false)}
        header={'Download Ruleset?'}
        subHeader={focusedRulesetName}
        message={'This will install this ruleset.'}
        buttons={[
          {
            text: 'Yes',
            handler: async () => {
              setDownloadRules(false)
              console.log("Downloading rules.")
              setLoading(true)
              await addRulesetToUser(focusedRulesetId, 
                focusedRulesetName,
                focusedRulesetVersion
              ).then(updateUserRulesList).finally(() => setLoading(false))
            }
          }, 
          {
            text: 'No'
          }
        ]}
      />
    )
  }

  const DeleteAlert = () => {
    return (
      <IonAlert
        isOpen={deleteRules}
        onDidDismiss={() => setDeleteRules(false)}
        header={'Delete Ruleset?'}
        subHeader={focusedRulesetName}
        message={'This will uninstall this ruleset.'}
        buttons={[
          {
            text: 'Yes',
            handler: async () => {
              setDeleteRules(false)
              console.log("Deleting rules.")
              console.log("Setting loading to true")
              setLoading(true)
              console.log("Loading set to true.")
              await removeRulesetFromUser(focusedRulesetId, 
                focusedRulesetName,
                focusedRulesetVersion
              ).then(updateUserRulesList).finally(() => setLoading(false))
            }
          }, 
          {
            text: 'No'
          }
        ]}
      />
    )
  }

  const InstalledRulesList = () => {
    if(userRulesList.length !== 0){
      return (
        <IonList lines="full" inset={true}>
        <IonListHeader lines="full">Installed Rules</IonListHeader>
        {userRulesList.map((rule: {id: string, name: string, version: string}) => (
          <IonItem key={rule.id + "-installed"}>
            <IonLabel>
              <h2>{rule.name}</h2>
              <h3>Version: {rule.version}</h3>
              <IonButton slot="end" onClick={() => openAlert(true, "delete", rule.id, rule.name, rule.version)}> 
                Delete
              </IonButton>
            </IonLabel>
          </IonItem>
        ))}
      </IonList>
      )
    }
    return null
  }

  const AvailableRulesList = () => { //Component that flickers
    console.log("Returning available rules.")
    console.log("Ruleslist: ",rulesList)
    return(
      <IonList lines="full" inset={true}>
        <IonListHeader lines="full">Available Rules</IonListHeader>
        {rulesList.map((rule: {id: string, data: {name: string, version: string}}) => (
          <RulesetItem key={rule.id + "ruleset-item"} id={rule.id} name={rule.data.name} version={rule.data.version}/>
        ))}
      </IonList>
    )
  }

  return (
    <IonPage>
      <MainHeader/>
      <IonContent className="ion-content-rulesets ion-padding" fullscreen>
        <InstalledRulesList/> {/*Custom list component*/}
        <AvailableRulesList/> {/*Custom list component*/}
        <DownloadAlert/>
        <DeleteAlert/> 
        <IonLoading duration={0} isOpen={loading}/>
      </IonContent>
    </IonPage>
  )
}

export default Rulesets

Hopefully that is enough information to go off of. I can add more if need be. Thanks in advance.

You’re updating state on alert open and close, which is going to cause a re-render. That shouldn’t necessarily cause a flicker, but the complexity of your state above is probably hiding some issues.

First thing to try would be deferring the state changes on open and close. Meaning try in openAlert to add a setTimeout or requestAnimationFrame around all the set methods.

An even better idea would be to refactor this into a bunch of smaller components. I find more than three useState in a component and things start to get unwieldy and it’s time to separate things into sub components.

Hey max thanks for the reply I truly appreciate it. I hadn’t had time to work on this project in a while and finally came back to it.

I managed to solve my issue by utilizing the alertController. It allowed me to get rid of all the setStates I was doing which without a doubt were causing the flickering like you stated.

It also allowed me to clean up my code greatly! Here’s the updated code for others for future reference:

import { IonContent, IonPage, IonButton, IonLoading, IonList, IonItem, IonLabel, IonListHeader, IonAlert } from '@ionic/react'
import React, { useEffect, useState } from 'react'
import '../css/Rulesets.css'
import '../css/Global.css'
import { MainHeader } from '../../components/Headers/tsx/MainHeader'
import { getUserRulesets, getRulesets, addRulesetToUser, removeRulesetFromUser } from '../../firebase/api/firestoreAPI'
import {alertController} from '@ionic/core'

const Rulesets: React.FC = () => {
  const [loading, setLoading] = useState(true)
  const [rulesList, setRulesList] = useState([])
  const [userRulesList, setUserRulesList] = useState([])
  const sleep = (milli: number) => {
    return new Promise(resolve => setTimeout(resolve, milli))
  }

  async function handleAlert(alertType: string, header: string, subheader: string, message: string, rulesetData: string[]) {
    
    if(alertType === "download"){
      const alert = await alertController.create({
        header: header,
        subHeader: subheader,
        message: message,
        buttons: [
          {
            text: 'Yes',
            handler: async () => {
                setLoading(true)
                await addRulesetToUser(rulesetData[0], rulesetData[1], rulesetData[2])
                await sleep(1000)
                await updateUserRulesList()
                setLoading(false)
            }
          },
          {
            text: 'No'
          }
        ]
      })

      await alert.present()
    }
    else{
      const alert = await alertController.create({
        header: header,
        subHeader: subheader,
        message: message,
        buttons: [
          {
            text: 'Yes',
            handler: async () => {
                setLoading(true)
                await removeRulesetFromUser(rulesetData[0], rulesetData[1], rulesetData[2])
                await sleep(1000)
                await updateUserRulesList()
                setLoading(false)
            }
          },
          {
            text: 'No'
          }
        ]
      })
      
      await alert.present()
    }

  }

  useEffect(() => {
    getUserRulesets().then((rulesets: any) => {
      if(rulesets){
        setUserRulesList(rulesets)
      }
    })
    .then(() => getRulesets().then((rulesets: any) => {
      if(rulesets){
        setRulesList(rulesets.map((rule: {rule: []}) => rule))
      }
    }))
    .finally(() => setLoading(false))
  }, [])

  async function updateUserRulesList(){
    setLoading(true)
    await getUserRulesets().then((rulesets: any) => {
      if(rulesets){
        setUserRulesList(rulesets)
      }
      else{
        setUserRulesList([])
      }
    })
  }

  const RulesetItem = (prop: {id: string, name: string, version: string}) => {
    var installed = false
    var rule: {id: string}

    for(rule of userRulesList){
        if(rule.id === prop.id){
          installed = true
        }
      }
    
    if(installed){
      return (
        <IonItem key={prop.id+ "-available"}>
        <IonLabel>
          <h2>{prop.name}</h2>
          <h3>Version: {prop.version}</h3>
          <p>Installed</p>
        </IonLabel>
      </IonItem>
      )
    }
    return (
    <IonItem key={prop.id+ "-available"} button={true} onClick={() => handleAlert("download", 'Download Ruleset?', prop.name, 
    'This will install this ruleset.', [prop.id, prop.name, prop.version])}>
      <IonLabel>
        <h2>{prop.name}</h2>
        <h3>Version: {prop.version}</h3>
      </IonLabel>
    </IonItem>
    )    
  }

  const InstalledRulesList = () => {
    if(userRulesList.length !== 0){
      return (
        <IonList lines="full" inset={true}>
        <IonListHeader lines="full">Installed Rules</IonListHeader>
        {userRulesList.map((rule: {id: string, name: string, version: string}) => (
          <IonItem key={rule.id + "-installed"}>
            <IonLabel>
              <h2>{rule.name}</h2>
              <h3>Version: {rule.version}</h3>
              <IonButton slot="end" onClick={() => handleAlert("delete", "Delete Ruleset?", rule.name, "This will uninstall this ruleset.", [rule.id, rule.name, rule.version])}> 
                Delete
              </IonButton>
            </IonLabel>
          </IonItem>
        ))}
      </IonList>
      )
    }
    return null
  }

  const AvailableRulesList = () => { 
    return(
      <IonList lines="full" inset={true}>
        <IonListHeader lines="full">Available Rules</IonListHeader>
        {rulesList.map((rule: {id: string, data: {name: string, version: string}}) => (
          <RulesetItem key={rule.id + "ruleset-item"} id={rule.id} name={rule.data.name} version={rule.data.version}/>
        ))}
      </IonList>
    )
  }

  return (
    <IonPage>
      <MainHeader/>
      {loading ? <IonLoading duration={0} isOpen={loading}/>:       
        <IonContent className="ion-padding ion-content-global">
          <InstalledRulesList/>
          <AvailableRulesList/>
        </IonContent>
      }
    </IonPage>
  )
}

export default Rulesets

Thanks again and I apologize for taking so long to get back to you. We can close this thread.