plutownium
plutownium

Reputation: 1988

Can I get React to notice when a property on an object in an array passed to props changes its value?

I am trying to avoid extra work by adding a property to an object in an array of objects onMouseEnter when I mouse over the ApartmentCard displaying info about a given apartment.

There is also a map beside the list of ApartmentCards. The map has a marker for each apartment. My goal is to make React retrigger useEffect so that all the old markers are removed, and new ones are placed, including one that will be highlighted to indicate it is the associated marker.

(As an aside, it's possible selecting the appropriate marker and updating its color would be more efficient, but that's for later)

So currently in my ApartmentCard.tsx I have:

interface ApartmentCardProps {
    apartment: IHousing;
    addr: string;
    gyms: IAssociation[];

    apUrl: string;
}

const ApartmentCard: React.FC<ApartmentCardProps> = ({ apartment, addr, gyms, apUrl }) => {
    return (
        <div
            onMouseEnter={() => {
                console.log("mouse enter", "23rm");
                apartment.isHighlighted = true;
            }}
            onMouseLeave={() => {
                apartment.isHighlighted = false;
            }}
        >Apartment address, other data</div>
 };

export default ApartmentCard;

On the IHousing interface there is some predictable stuff that I left out, but also:

export interface IHousing {
    type: "apartment" | "house";
    // snip
    isHighlighted?: boolean;
}

ApartmentCard is a child component of MapPage. Map is also a child of MapPage. Both receive the same data about apartments, so when isHighlighted is updated in ApartmentCard, the objects used to populate Map immediately update their property and values with isHighlighted = true or false.

Here's some code from Map.tsx:

interface MapboxProps {
    center: [number, number];
    qualifiedFromCurrentPage: IHousing[];
    // zoom: number;
}

const Map: React.FC<MapboxProps> = ({ center, qualifiedFromCurrentPage }) => {
    const [markers, setMarkers] = useState<mapboxgl.Marker[]>([]);
    const { isOpen, toggleIsOpen } = useContext(SidebarStateContext) as ISidebarContext;

    // initialization of map has been removed to save space in the post

    // plot qualified gyms and apartments
    useEffect(() => {
        if (map === null) return;

        let allMarkers: mapboxgl.Marker[] = [];
        if (qualifiedFromCurrentPage.length !== 0 && map.current) {
            const { apartmentMarkers, gymMarkers } = unpackMarkers(qualifiedFromCurrentPage);
            allMarkers = [apartmentMarkers, gymMarkers].flat();

            addNewMarkers(allMarkers, markers, setMarkers, map.current);
        }
        return () => {
            // remove all old markers
            for (const marker of allMarkers) {
                marker.remove();
            }
        };
    }, [map, qualifiedFromCurrentPage]);

    function unpackMarkers(pageMarkers: IHousing[]): { apartmentMarkers: mapboxgl.Marker[]; gymMarkers: mapboxgl.Marker[] } {
        // snip
    }

    function addNewMarkers(newMarkers: mapboxgl.Marker[], oldMarkers: mapboxgl.Marker[], markerUpdater: Function, map: mapboxgl.Map) {
           // snip
    }

    return (
        <div id="mapContainerOuter" className={`${decideWidth(isOpen, isOnMobile)} w-full mapHeight mr-2`}>
            <div id="mapContainer" ref={mapContainer}></div>
            <button
                onClick={() => {
                    console.log(qualifiedFromCurrentPage, "142");
                }}
            >
                Test
            </button>
        </div>
    );
};

export default Map;

What I was really hoping for was that the useEffect hook with qualifiedFromCurrentPage in its dependency array would run again when the property changes on one of its objects. But it ignores it.

One solution that for sure would work is if (1) I assigned a number to each apartment in the list for a given page then (2) let the ApartmentCard set the active value, probably stored in the parent MapPage, which would then (3) be passed down into the Map component and used to highlight the associated marker.

It could and would work. But, given that I'm supposed to be a professional React user, and I'm not getting an expected behavior, I would really like to know what's going on. Shouldn't adding a property to an object in an array cause React to re-run that useEffect hook? That's what I expected.

Thanks all

edit: I am feeding in a shallow copy to both apartmentCard and Map from this function in MapPage, but I thought it would be fine since the values of a shallow copy point to the same objects as the input array.

    function getCurrentPageResults(qualified: IHousing[], page: number): IHousing[] {
        if (qualified.length < 10) {
            return qualified;
        }
        const startOfPage = page * 10 - 10;
        const endOfPage = page * 10 - 1;
        return qualified.slice(startOfPage, endOfPage);
    }

Upvotes: 0

Views: 782

Answers (1)

Vikki
Vikki

Reputation: 321

I assume you're a former Vue user? React doesnt watch the change of object or variables, in fact, you have to notify React that some value changes by setState or state hook, and then React will handle the virtual dom result diff and rerender the real dom only in need.

React's one-way data flow (also called one-way binding, opposite to two-way binding) keeps everything modular and fast. You can check thinking-in-react for more detail.

Upvotes: 3

Related Questions