Home > Software engineering >  React: How to display a modal without encountering error message (details provided)
React: How to display a modal without encountering error message (details provided)

Time:04-16

Intro

When my application starts, a list of images appear. If additional details about an image is required, a user can click on an image that has previously loaded, and a modal would appear with it's relevant details.

Even though everything works as it should, I'm encountering an error message that can be found in Chrome browser's console.

Error

Warning: Each child in a list should have a unique "key" prop.

Attempted

I've tried placing key={Math.floor(Date.now() * 20)} within the Image found in the modal, but that doesn't work.

Where am I going wrong? How can I fix this issue?

  function getHomePage() {
  const [token, setToken] = useState("");
  const [visibility, setVisibility] = useState(false);
  const [NFTBalances, setNFTBalances] = useState();
  const [collection, setCollection] = useState();
  const [nft, setNft] = useState();
  const { Moralis } = useMoralis();

  //load my images when application starts
  useEffect(() =>  {
    collectionChanged('myCollection');
  }, []);
    
const handleSelectToken = async (num, col) => {
    if (num && col) {
      const dbNFTs = Moralis.Object.extend(col);
      const query = new Moralis.Query(dbNFTs);
      console.log(num);
      query.equalTo("tokenId", num);
      let selectedNFT = await query.first();
      selectedNFT = selectedNFT.attributes;
      console.log(selectedNFT);
      setNft(selectedNFT);
      setVisibility(true);

      //open modal
      $("#openModal").modal("show");
    }
  };


  const addToNFTs = async (col) => {
    const dbNFTs = Moralis.Object.extend(col);
    const query = new Moralis.Query(dbNFTs);
    query.ascending("rank");
    query.limit(4);
    const topNFTs = query.skip(NFTBalances.length);
    const results = await topNFTs.find();
    setNFTBalances(NFTBalances.concat(results));
  }

return (
  <>
//modal box
      <div className="modal fade" id="openModal" tabIndex="-1" role="dialog" aria-hidden="true">
           <div className="modal-dialog modal-lg" role="document">
              <div className="modal-content">
                 <div className="modal-header">
                    <button type="button" className="close" 
                         data-dismiss="modal" aria-label="Close">
                       <span aria-hidden="true">&times;</span>
                    </button>
                 </div>
                 <div className="modal-body">
                   <div>
                     <Image key={Math.floor(Date.now() * 20)} 
                         preview={false} src={nft.image} loading="lazy"
                         style={{ display: "block", width: "50%"}}/>    

                       <p>{nft.details}</p> 
                   </div>
                 </div>
              </div>
           </div>
     </div>

 
               <div className="col-xxl-3 col-xl-3 col-lg-3 col-md-3">
                    <div className="filter-sidebar">
                       <div className="filter-sidebar-content">
                          <div className="form-group">

                             //SEARCH BOX
                             <Search placeholder="Please search here"
                                   onChange={(e) => setToken(e.target.value) }
                                   onSearch={() => handleSelectToken(token, collection)} />
                          </div>
                       </div>
                    </div>
                 </div>

   

//Images populate when application loads
//Clicking an image will populate image details in a modal
    <div className="row">
          {NFTBalances && NFTBalances.map((nft, index) => {
             return (
               <div className="col-xxl-3 col-xl-3 col-lg-6 col-md-6">
                 <div className="card items">
                   <Card key={index} onClick={() => 
                      handleSelectToken(nft.attributes.tokenId,collection)}
                          cover={ <Image src={nft.attributes.image} /> }>
                   </Card>
                  </div>     
                </div> 
                 );})}
   </div>          
   </>        
      );}

export default getHomePage;

CodePudding user response:

You're placing the key at the wrong place. You're not rendering a list of images, but rather a list of cards containing images, here is where you need to add the key prop. You've already added a key to the card itself, butit needs to be present on the parent element. You can use index only if you do not expect the order of these to change during render. Alternatively use your key={Math.floor(Date.now() * 20)}.

        {NFTBalances && NFTBalances.map((nft, index) => {
         return (
           <div key={index} className="col-xxl-3 col-xl-3 col-lg-6 col-md-6">
             <div className="card items">
               <Card onClick={() => 
                  handleSelectToken(nft.attributes.tokenId,collection)}
                      cover={ <Image src={nft.attributes.image} /> }>
               </Card>
              </div>     
            </div> 
             );})}
           </div>          
          </>        
         );}

CodePudding user response:

In modal box the key for the <Image> is causing an issue since the key values need to be unique.

By setting key={Math.floor(Date.now() * 20)}, every <Image> will end up with the same key since they are rendered at the same time.

You should be able to just remove key from that line:

<Image preview={false} src={nft.image} loading="lazy" style={{ display: "block", width: "50%"}}/>

But if that ends up giving you issues, you could also generate a key randomly:

<Image key={Math.random().toString()} preview={false} src={nft.image} loading="lazy" style={{ display: "block", width: "50%"}}/>

I don't know what your nft object looks like, but if each one has its own unique id, that id would be ideal to use for key. This answer explains why that is.

<Image key={nft.id} preview={false} src={nft.image} loading="lazy" style={{ display: "block", width: "50%"}}/>

Another issue is that there is no key being set on the immediate child inside of .map. You should be setting the key on the div that comes right after return:

{NFTBalances && NFTBalances.map((nft, index) => {
             return (
               <div key={index} className="col-xxl-3 col-xl-3 col-lg-6 col-md-6">
                 <div className="card items">
                   <Card onClick={() => 
                      handleSelectToken(nft.attributes.tokenId,collection)}
                          cover={ <Image src={nft.attributes.image} /> }>
                   </Card>
                  </div>     
                </div> 

Good luck! They key prop is pretty important in React. Here's a bit more information from a great answer about key if you want to learn more.

CodePudding user response:

{NFTBalances && NFTBalances.map((nft, index) => {
  return (
    <div className="col-xxl-3 col-xl-3 col-lg-6 col-md-6">

Keys are needed when you make an array of elements, and so this <div> is where you need to put the key. Ideally, it will be some unique identifier found in the nft object. Since i don't know the structure of your data i can't be sure the best value to use, but assuming nft.attributes.tokenId is unique and always present:

<div key={nft.attributes.tokenId} className="col-xxl-3 col-xl-3 col-lg-6 col-md-6">

If there is no unique identifier on nft, then as a last resort you could use the index, but this will only work correctly if you are not rearranging this list.

<div key={index} className="col-xxl-3 col-xl-3 col-lg-6 col-md-6">

key={Math.floor(Date.now() * 20)}

This is a bad key. First, it's not going to be unique: all these <div>s are being created at basically the same time, so in all likelyhood they'll all get the same value for Date.now(). Second, they are going to change from one render to the next.

The point of keys in react is to tell react which element in the first render corresponds to which element in the second render. If they're all changing, then react thinks it needs to throw out everything and start again (ie, it must unmount and remount all the components)

  • Related