Question

Re-render modal component onClick with random value parameters

i'm new to ReactJS. And I'm trying to create an app which displays a modal with different parameters. My problem is, I have a reusable modal component, but how do I achieve when pressing/clicking a button inside the modal will re-render the whole modal component with different results/value?

Here is my code:

Modal.js

import { useEffect, useRef, Fragment, Link, useState } from "react";
import { useParams, useNavigate, useLocation } from "react-router-dom";
import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock";
import airportData from '../airports.json';

export function Modal() {
    const modalRef = useRef();
    const { id } = useParams();
    const navigate = useNavigate();
    const [reload, setReload] = useState(false);

    useEffect(() => {
        const observerRefValue = modalRef.current;
        disableBodyScroll(observerRefValue);

        return () => {
            if (observerRefValue) {
                enableBodyScroll(observerRefValue);
            }
        };
    }, []);

    function setImageUrlLarge(url) {
        return {
            backgroundImage: `url(${process.env.PUBLIC_URL}/assets/images/large/${url}.jpg)`
        }
    }

    return (
        <div ref={modalRef} className="modal-wrapper">
            <div className="modal">
                <div className={`detail ${id} overlay`} style={setImageUrlLarge(id)}>
                    <a className='overlay' rel='noopener'></a>
                    <AirportDetails id={id} airportData={airportData} />
                </div>
            </div>
        </div>
    )

    function AirportDetails({id, airportData}) {
        const [data, setData] = useState(airportData);
        const [airportId, setAirportId] = useState(id);


        const airport = airportData.filter((airport) => airport.id.toLowerCase().includes(id.toLowerCase()))[0];

        const [description, setDescription] = useState(airport.description);

        const pattern = /\*([A-Za-z])\*/gi;
        const em = description.replace(pattern, '<em>$1</em>');  
    
        const currUrl = window.location.href;

        function capitalize(str) {
            return str.toUpperCase();
        }

        const share = (socialType) => e => {
            if (socialType == "twitter") {
                const text = `Making sense of those three-letter airport codes: ${capitalize(id)}`;
                const link = `https://twitter.com/intent/tweet?url=${currUrl}&text=${text}`;
                return link;
            }
            const link = `https://www.facebook.com/dialog/share?display=popup&href=${currUrl}${id}&redirect_uri=${currUrl}${id}`;
            return link;
        }

        function setTo(social) {
            if(social=="twitter") {
                return "https://twitter.com/intent/tweet?url=$SHARE_URL&text=$TEXT";
            }  
            else {
                return "https://www.facebook.com/sharer/sharer.php?u=$SHARE_URL";
            }
        }

        return (
                <div className='container'>
                    <div className='detail-info'>
                        <h1>{airport.id}</h1>
                        <h2>{airport.name}</h2>
                        <h3><span className="local_name">{airport.local_name}</span></h3>
                        <h4>{airport.city}</h4>
                        <div className="description fl-edu">
                            <p dangerouslySetInnerHTML={{ __html: em}}></p>
                        </div>
                        <a className="close-detail" role="button" onClick={() => navigate('/')}></a>
                        <a className="random" role="button" onClick={() => {randomAirport()}}>
                        Random Airport</a>
                         <div className="social">
                            <a role="button" className="twitter" href={setTo("twitter")} onClick={() => {share("twitter")}} target="_blank"></a>
                        </div>
                        <div className="social">
                            <a className="facebook" href={setTo("facebook")} onClick={() => {share("facebook")}} target='_blank'></a>
                        </div>
                    </div>
                    <div className="photo-credit">
                        Photo by <a>{airport.imageCredit}</a>
                    </div>
                    <a className="back" role="button" onClick={() => navigate('/')}>Airport Codes PH</a>
                </div>
                
        )
    }

    function randomAirport() {
        const rand = Math.floor(Math.random() * airportData.length);
        const airportRandData = airportData[rand];
        setReload(!reload);
        console.log(reload);
    }
}

What i'm achieving right now is to change the state of the modal every time when the button is clicked and randomizing the value of an object. I'm expecting to reload the modal component with a different value from the randomAirport function and passing the airportData back to the Modal component.

 3  52  3
1 Jan 1970

Solution

 1

The idea should be not to rerender the modal with different content, but to simply keep it open and make the component it renders re-render with it's own new state/content/etc.

Refactor your code to:

  1. Separate & declare AirportDetails outside the Modal component so it's not redeclared each render cycle. This fixes a React anti-pattern. Remove the reload state, it's also an anti-pattern.

    import { useEffect, useRef, Fragment, Link, useState } from "react";
    import { useParams, useNavigate, useLocation } from "react-router-dom";
    import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock";
    import airportData from '../airports.json';
    
    export function Modal() {
      const modalRef = useRef();
      const { id } = useParams();
      const navigate = useNavigate();
    
      useEffect(() => {
        const observerRefValue = modalRef.current;
        disableBodyScroll(observerRefValue);
    
        return () => {
          if (observerRefValue) {
            enableBodyScroll(observerRefValue);
          }
        };
      }, []);
    
      function setImageUrlLarge(url) {
        return {
          backgroundImage: `url(${process.env.PUBLIC_URL}/assets/images/large/${url}.jpg)`
        }
      }
    
      return (
        <div ref={modalRef} className="modal-wrapper">
          <div className="modal">
            <div className={`detail ${id} overlay`} style={setImageUrlLarge(id)}>
              <a className='overlay' rel='noopener'></a>
              <AirportDetails id={id} airportData={airportData} />
            </div>
          </div>
        </div>
      )
    }
    
  2. Update AirportDetails to initialize the data state to a random airport's data, and update the onClick handler to to select a random airport's data and enqueue a state update. Render all the airport details from the local data state.

    function AirportDetails({ id, airportData }) {
      // Initialize data to specific airport by id,
      // otherwise select a random airport.
      const [data, setData] = useState(() => {
        const airport = airportData.find(
          (airport) => airport.id.toLowerCase().includes(id.toLowerCase())
        );
    
        return airport ? airport : randomAirport(airportData);
      });
    
      ...
    
      return (
        <div className='container'>
          <div className='detail-info'>
            <h1>{data.id}</h1>
            <h2>{data.name}</h2>
            <h3><span className="local_name">{data.local_name}</span></h3>
            <h4>{data.city}</h4>
            <div className="description fl-edu">
              <p dangerouslySetInnerHTML={{ __html: em}}></p>
            </div>
            <a
              className="random"
              role="button"
              onClick={() => {
                // Enqueue state update to new random airport data
                setData(randomAirport(airportData));
              }}
            >
              Random Airport
            </a>
            ...
          </div>
          <div className="photo-credit">
            Photo by <a>{data.imageCredit}</a>
          </div>
          ...
        </div>
      )
    }
    
    function randomAirport(airportData) {
      const rand = Math.floor(Math.random() * airportData.length);
      return airportData[rand];
    }
    

Alternative

Since the id route path parameter and the airports.json data are used to compute the derived state in AirportDetails, an improvement I can suggest is to not copy any data into any local state (this is a bit of a React anti-pattern anyway) and directly compute the matching airport data based on the current id value, and to simply navigate to a new random airport id value.

Example:

import { useEffect, useRef } from "react";
import { useParams } from "react-router-dom";
import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock";

export function Modal() {
  const modalRef = useRef();
  const { id } = useParams();

  useEffect(() => {
    const observerRefValue = modalRef.current;
    disableBodyScroll(observerRefValue);

    return () => {
      if (observerRefValue) {
        enableBodyScroll(observerRefValue);
      }
    };
  }, []);

  function setImageUrlLarge(id) {
    return {
      backgroundImage: `url(${process.env.PUBLIC_URL}/assets/images/large/${id}.jpg)`
    }
  }

  return (
    <div ref={modalRef} className="modal-wrapper">
      <div className="modal">
        <div className={`detail ${id} overlay`} style={setImageUrlLarge(id)}>
          <a className='overlay' rel='noopener'></a>
          <AirportDetails />
        </div>
      </div>
    </div>
  )
}
import { useParams, useNavigate, generatePath } from "react-router-dom";
import airportData from '../airports.json';

function randomAirportId() {
  const rand = Math.floor(Math.random() * airportData.length);
  return airportData[rand].id;
}

function AirportDetails() {
  const { id } = useParams();
  const navigate = useNavigate();

  const airport = React.useMemo(() => {
    return airportData.find(
      (airport) => airport.id.toLowerCase().includes(id.toLowerCase())
    );
  }, [id]);

  ...

  if (!airport) {
    return null; // or navigate back, etc.
  }

  return (
    <div className='container'>
      <div className='detail-info'>
        <h1>{airport.id}</h1>
        <h2>{airport.name}</h2>
        <h3><span className="local_name">{airport.local_name}</span></h3>
        <h4>{data.city}</h4>
        <div className="description fl-edu">
          <p dangerouslySetInnerHTML={{ __html: em}}></p>
        </div>
        <a
          className="random"
          role="button"
          onClick={() => {
            navigate(
              generatePath("/airport/:id", { id: randomAirport()}
),
              { replace: true }
            );
          }}
        >
          Random Airport
        </a>
        ...
      </div>
      <div className="photo-credit">
        Photo by <a>{airport.imageCredit}</a>
      </div>
      ...
    </div>
  )
}
2024-07-25
Drew Reese

Solution

 0

To re-rendering the modal component, you need to update the current state in the Modal component and pass that state down to the desired component, in your project that is AirportDetails component, you have to create this component separately and pass data to the "AirportDetails" by props.

In your parent component, that means where all these data are generating add we have get the unique id for the airport,

const [currentAirportId, setCurrentAirportId] = useState(id);

Then we have to update data props,

        <div className="modal">
            <div className={`detail ${currentAirportId} overlay`} style={setImageUrlLarge(currentAirportId)}>
                <a className='overlay' rel='noopener'></a>
                <AirportDetails id={currentAirportId} airportData={airportData} setReload={randomAirport} />
            </div>
        </div>

Also we need to update random airport function to set "current airport id"

  setCurrentAirportId(airportRandData.id);

ok, Now for the separate component called AirportDetails component, we need to get those sending values from parent.

export function AirportDetails({ id, airportData, setReload }) {

To find airport data you can reuse your method,

const airport = airportData.find((airport) => airport.id.toLowerCase() === id.toLowerCase());

Also to share your links u can use window.open

const share = (socialType) => (e) => {
    //
    window.open(link, '_blank');
};

For reload u can add your random airport button

<a className="random" role="button" onClick={() => setReload(prev => !prev)}>Random Airport</a>
2024-07-25
shanaka prince