4

I have a typescript function that receives a 'queryParameter' and then loops over an existing set of queryParameters from a state hook within the component. If it finds a matching parameter name I want it to update the queryParameters object into the const 'newQueryParams'.

note that the issue only occurs with the 'Date' query parameter. All code examples work fine for both other types ('getLatest' and 'fundCode'). Also this is not a question of date object equality

function (only the necessary part):

  const handleQueryParameterChange = (queryParameter: DataProductQueryParameter) => {

    console.log("existing parameters from state hook: ", queryParameters)
    console.log("incoming parameter to function: ", queryParameter)

    const newQueryParams = queryParameters.map((param, i) => {
      console.log("loop: " + i);
      if(queryParameter.name === param.name) {
        console.log("match");
        param.suggestedValue = queryParameter.suggestedValue;
        }
      return param;
  });

  console.log("new query params: ", newQueryParams);

I have logged the various bits to show what happens:

log output

As you can see the incoming parameter tries to change the date to the 21st, but fails, despite a match in the loop.

I have also tried using deep copies:

const deepCopyFunction = (inObject: any) => {
  let outObject: any, value, key

  if (typeof inObject !== "object" || inObject === null) {
    return inObject // Return the value if inObject is not an object
  }

  // Create an array or object to hold the values
  outObject = Array.isArray(inObject) ? [] : {}

  for (key in inObject) {
    value = inObject[key]

    // Recursively (deep) copy for nested objects, including arrays
    outObject[key] = deepCopyFunction(value)
  }

  return outObject
}

    const handleQueryParameterChange = (
    queryParameter: DataProductQueryParameter
  ) => {
    
    const queryParametersCopy = deepCopyFunction(queryParameters);
    const deepCopyQueryParam = {...queryParameter};

    console.log("existing parameters copy: ", queryParametersCopy)
    console.log("incoming new parameter: ", queryParameter)

  
    console.log("incoming parameter deep copy: ", deepCopyQueryParam);

    const newQueryParams = queryParametersCopy.map((param, i) => {
      console.log("loop: " + i);
    if(deepCopyQueryParam.name === param.name) {
      console.log("match");
      param.suggestedValue = deepCopyQueryParam.suggestedValue;
    }
     return param;
  });

  console.log("new query params: ", newQueryParams);

which produced the same:

logging output with deep copies

I have also tried just switching out the whole object when i get the match, rather than just editing a property:

const handleQueryParameterChange = (queryParameter: DataProductQueryParameter) => {
    
    console.log("existing parameters: ", queryParameters)
    console.log("incoming new parameter: ", queryParameter)

    const newQueryParams = queryParameters.map(param => {
      return queryParameter.name === param.name ? queryParameter : param;    
  });

  console.log("new query params: ", newQueryParams);

...which didnt work either/ The below screenshot outlines the output, but also note the difference between the headline and expanded object in "incoming new parameter". I've seen this a few times and while stack overflow tells me its the async nature of the console, i can't figure out why its happening in the code.

logging output for object switch rather than update

again please note that the issue only occurs with the 'Date' query parameter. All code examples work fine for both other types ('getLatest' and 'fundCode')

Update

I have implemented one of the suggested improvements, but I'm still not getting the right output.

Here is a little more of the code, including a state hook and an effect hook, with the new implementation:


    const [queryParameters, setQueryParameters] = useState<DataProductQueryParameter[]>(dataProductQueryParameters ?? []);

    useEffect(() => {
    console.log("effect called: ", queryParameters)
  }, [queryParameters])

  const handleQueryParameterChange = (queryParameter: DataProductQueryParameter) => {

  if(queryParameter.name === "getLatest") {
    setDatePickerDisabled(!datePickerDisabled);
  } 

  const matchIndex = queryParameters.findIndex(param => queryParameter.name === param.name);
      const queryParametersCopy = JSON.parse(JSON.stringify(queryParameters));  

  if (matchIndex !== -1) {
    const suggestedValue = queryParameter.suggestedValue;
    queryParametersCopy[matchIndex] = { ...queryParametersCopy[matchIndex], suggestedValue}
  }

    console.log("new query params: ", queryParametersCopy);

    setQueryParameters(queryParametersCopy);
  };

The state hook sets successfully on component load. When I invoke the handleChange function, my debugger shows that it updates the new array (newQueryParams) successfully.

The issue arises when setQueryParameters is called to update the state hook. It seems to trigger fine as the useEffect hook gets called. But both my debugger and console show that the suggestedValue field on the updated array doesnt get updated.

Again - this works absolutely fine for the other fields - getLatest and fundCode, so the logic is sound and the hooks are firing for setting new values, but the date one simply won't update.

Thanks

PeteG
  • 421
  • 3
  • 17
  • 1
    Two `Date` objects with the same value are not equal unless they point to the same reference. In other words `new Date(2021, 0, 1) !== new Date(2021, 0, 1)` is true. You need to use `variable.valueOf() === variable.valueOf()` to compare dates. – Heretic Monkey Mar 23 '21 at 12:32
  • 1
    Does this answer your question? [Compare two dates with JavaScript](https://stackoverflow.com/questions/492994/compare-two-dates-with-javascript) – Heretic Monkey Mar 23 '21 at 12:32
  • Afraid not, I never test for date equality, only the name field on the object – PeteG Mar 23 '21 at 12:35
  • 4
    I see no reason why this would not work, are you 100% sure that your console.log is not out of date ? what does it say if you console.log(newQueryParams.map(p => p. suggestedValue)) – Woody Mar 26 '21 at 16:18
  • 2
    Could you please add a [codesandbox](https://codesandbox.io/s/react-new) that illustrate your problem? – johannchopin Apr 01 '21 at 08:04

2 Answers2

2

Your initially code works perfectly fine. If you run the following snippet you will see that the final new query params log show that the suggestedValue attribute has been updated successfully:

const queryParameters = [{
    name: 'test',
    suggestedValue: '2021-03-23'
  },
  {
    name: 'foobar',
    suggestedValue: '2022-03-23'
  }
]

const handleQueryParameterChange = (queryParameter) => {

  console.log("existing parameters from state hook: ", queryParameters)
  console.log("incoming parameter to function: ", queryParameter)

  const newQueryParams = queryParameters.map((param, i) => {
    console.log("loop: " + i);
    if (queryParameter.name === param.name) {
      console.log("match");
      param.suggestedValue = queryParameter.suggestedValue;
    }
    return param;
  });

  console.log("new query params: ", newQueryParams);

}

handleQueryParameterChange({
  name: 'test',
  suggestedValue: '2021-03-21'
})

However you could improve this code to avoid parsing all the elements of the array with .map(). You should better use the .findIndex() method for that. If you run the following snippet you will see that it will only log loop: 0:

const queryParameters = [{
    name: 'test',
    suggestedValue: '2021-03-23'
  },
  {
    name: 'foobar',
    suggestedValue: '2022-03-23'
  }
]

const handleQueryParameterChange = (queryParameter) => {

  console.log("existing parameters from state hook: ", queryParameters)
  console.log("incoming parameter to function: ", queryParameter)

  const matchIndex = queryParameters.findIndex((param, i) => {
    console.log("loop: " + i);
    return queryParameter.name === param.name
  })

  if (matchIndex !== -1) {
    const suggestedValue = queryParameter.suggestedValue
    queryParameters[matchIndex] = { ...queryParameters[matchIndex],
      suggestedValue
    }
  }

  console.log("new query params: ", queryParameters);
}

handleQueryParameterChange({
  name: 'test',
  suggestedValue: '2021-03-21'
})
johannchopin
  • 13,720
  • 10
  • 55
  • 101
2

I've tried to reproduce your setup with a small component having 2 states like you showed on your updated question. This code (which reuses your object swap) seems to update queryParameters properly. Could you try to reuse the part with setQueryParameters(prevQueryParameters => ...) to see if it solves your problem?

import React, { FC, Fragment, useCallback, useEffect, useState } from 'react';

interface DataProductQueryParameter {
  name: string;
  parameterType: string;
  format: string;
  description: string;
  suggestedValue: string;
}

const dataProductQueryParameters: DataProductQueryParameter[] = [
  {
    name: 'test',
    format: '',
    description: '',
    parameterType: '',
    suggestedValue: '',
  },
  {
    name: 'other',
    format: '',
    description: '',
    parameterType: '',
    suggestedValue: '',
  },
];

export const StackOverflow: FC = () => {
  const [datePickerDisabled, setDatePickerDisabled] = useState(false);
  const [queryParameters, setQueryParameters] = useState<DataProductQueryParameter[]>(
    dataProductQueryParameters,
  );

  useEffect(() => {
    console.log('effect called: ', queryParameters);
  }, [queryParameters]);

  const handleQueryParameterChange = useCallback(
    (queryParameter: DataProductQueryParameter) => {
      if (queryParameter.name === 'getLatest') {
        setDatePickerDisabled(prevDatePickerDisabled => !prevDatePickerDisabled);
      }

      setQueryParameters(prevQueryParameters => 
        prevQueryParameters.map(param => {
          if (param.name === queryParameter.name) {
            return queryParameter;
          }
          return param;
        }),
      );       
    },
    [setQueryParameters, setDatePickerDisabled],
  );

  return (
    <Fragment>
      <button
        onClick={() =>
          handleQueryParameterChange({
            name: 'test',
            description: 'updated',
            format: 'updated',
            parameterType: 'updated',
            suggestedValue: 'updated',
          })
        }
      >
        Update !
      </button>
    </Fragment>
  );
};
PierreB
  • 489
  • 3
  • 14