Search code examples
javascriptarraysreactjsreact-props

React clone an object from parent props keeps getting updated on re-render


Not sure what the fix is for this. We get an array of objects from our API and use this to create data in the view. At the same time in a child component (filters) I want to map over one of the fields in the data to create my filter categories. The filter will call new data from the API with a param, and that data is returned and updates the props (I think this is the issue as this reredners the child component using the filtered data to set the categories again).

I thought putting the array inside useEffect would stop it being updated.

Here is the current tsx in my Filter Component:

const ExclusiveOffersFilters = (props: ExclusiveOffersFiltersProps): JSX.Element => {
  const newFilters = JSON.parse(JSON.stringify(props.options)); // copy the object from props
  let filterOffers;

  useEffect(() => {
    filterOffers = newFilters.map((item: any) => {
      return { value: item.offerType, display: item.offerType };
    });
    console.log('filterOffers: ', filterOffers); // succesfully logs the filtered items. 
  }, []);

  return (
    <tr>
      <th>
        <Filter
          options={filterOffers} // here filterOffers is undefined
          alignRight={props.alignRight}
          handleClick={props.handleFilterChange}
          multiSelect={props.multiSelect}
          selectedItems={props.selectedItems}
        />
      </th>
    </tr>
  );
};

I also tried const newFilters = [...props.options]; and newFilters = props.options.slice(0) but it is the same issue (the wisdom seemed to be to use JSON parse/stringify).

In my parent component I am calling the child like so:

<ExclusiveOffersFilters handleFilterChange={props.handleFilterChange} options={props.options} />

If I remove the useEffect function and just update the filters it will work, however after selecting one filter the app updates the api call, only receives filtered data back and therefore only one category is set in the filters. Which I need to prevent.

I need a way to get the original categories and freeze them from being updated.

EDIT

Here is our sample data:

const data = [
  {
    offerId: 1,
    partnerName: "Another offer 4",
    imageUrl:
      "https://www.auctsjpg-w263-h98-99382466814e33da0cfa3d5d2afd921c.jpg",
    description:
      "Save up to 10% on something. <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> <p>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>",
    header: "Save up to 10%",
    enabled: true,
    deleted: false,
    offerType: "Motoring"
  },
  {
    offerId: 0,
    partnerName: "Amazing offer 4",
    imageUrl: "https://www.cGen",
    description:
      "Save up to 20% off* your business insurance thanks to our partnership with<BR/> Churchill Expert. Simply visit the Churchill Expert web page and choose the<BR/> insurance product to suit your business.<BR/>     Churchill Expert will automatically apply the discount to your premium for the<BR/>duration of your policy.     <BR/><BR/>*Minimum premiums apply. Discount applies to NIG policies arranged by Churchill Expert <BR/>and underwritten by U K Insurance Limited.<BR/> Professional Indemnity excluded.",
    header: "Save up to 20% off",
    enabled: true,
    deleted: false,
    offerType: "Insurance"
  },
  {
    offerId: 190,
    partnerName: "Amazing offer 4",
    imageUrl: "https://www.ch.Code=ExpertGen",
    description:
      "Save up to 20% off* your business insurance thanks to our partnership with<BR/> Churchill Expert. Simply visit the Churchill Expert web page and choose the<BR/> insurance product to suit your business.<BR/>     Churchill Expert will automatically apply the discount to your premium for the<BR/>duration of your policy.     <BR/><BR/>*Minimum premiums apply. Discount applies to NIG policies arranged by Churchill Expert <BR/>and underwritten by U K Insurance Limited.<BR/> Professional Indemnity excluded.",
    header: "Save up to 20% off",
    enabled: true,
    deleted: true,
    offerType: "Insurance"
  }
];

export default data;

If we filter on offerType = Motoring then we call our api again with Motoring as our filter this will only return 1 record from the above data, but now our filter gets updated and only shows 1 filter category of Motoring.

See attached examples from the app.

enter image description here enter image description here

I have dumped most of the files here: https://codesandbox.io/s/charming-sinoussi-l2iwh?file=/data.js:0-2061 with sample data. I don't know how helpful this is, it is hard to make a working example when there are so many dependencies.

The parent index.tsx handles all of the ajax api calls and passes down data into the props. The ExclusiveOffersFilters.tsx receives this and gets updated, which is the part I want to stop. As @Adam points out in the comments we are trying to fix this, but I think actually the parent is fine, it is the child that is the issue.

We do want to apply this logic elsewhere in the app, so performance would be good. I am wondering if I just need to not use props.options and use a different prop for this?


Solution

  • This is all you need. If you run into performance issues, use useMemo. If you don't run into performance issues, don't do anything

    const ExclusiveOffersFilters = (props: ExclusiveOffersFiltersProps): JSX.Element => {
    
      const filters = props.options.map((item) => ({
        value: item.offerType, 
        display: item.offerType
      }));
    
      return (
        <tr>
          <th>
            <Filter
              options={filters}
              alignRight={props.alignRight}
              handleClick={props.handleFilterChange}
              multiSelect={props.multiSelect}
              selectedItems={props.selectedItems}
            />
          </th>
        </tr>
      );
    };
    

    Example with useMemo

     const filters = useMemo(() => props.options.map(....),[props.options]);
    

    I cannot stress enough to stay away from useMemo unless you're absolutely sure you need it.