reactjs

changing the useState's value doesn't re-render the HTML code that depends on the state value


I have a React page with multiple subcomponents.

One of the subcomponent has 3 buttons with their classNames (disabled/enabled) dependent on the useState defined in the parent component.

I'm generating the buttons on the parent component and pass the entire React.Node to the child component that displays the button.

On clicking the buttons first time, the buttons update the class to the expected value.

However, when clicking recurrent times, even if the useState changes, it does not refreshes the classes of the buttons that already rendered.

export default function MyPage(){

const [activeCurrencies, setActiveCurrencies] = useState<[]>([]);

const updateActiveCurrencies = (currency) => {
    let bankCurrencies = activeCurrencies|| [];
    // remove the clicked currency from the existing list
    if (activeCurrencies.includes(currency)) {
      bankCurrencies = activeCurrencies.filter((c) => c != currency);
    } else {
    // Add clicked currency to the existing currencies list
      bankCurrencies.push(currency);
    }
    // update the useState with the new list of currencies
    setActiveCurrencies(bankCurrencies);
};

const generateCurrencyButtons = () => {
    const currencies = ["EUR", "USD", "GBP"];

    return (
      <div>
        {currencies.map((currency: string) => (
          <Button
            key={currency}
            variant="brandnavy"
            size="sm"
            className={
              activeCurrencies.includes(currency)
                ? "opacity-100"
                : "opacity-50"
            }
            onClick={() => updateActiveCurrencies(currency)}
          >
            {currency}
          </Button>
        ))}
      </div>
    );
  };

return (
   <div>
      <PageHeader
         activeCurrencies={generateCurrencyButtons()}
      >
         ...
      </PageHeader> 
      <PageContent>
         ...
      </PageContent
   </div>
)

}

The code above works first time, meaning that the buttons receive the opacity-50 on the first click on any buttons. However, even if the useStates updates when I click recurrent times, the buttons do not activate back to opacity-100

Can anyone help me what I am doing wrong here?

I have tried to use useEffect to capture when the useState changes but react complains about it:

Rendered more hooks than during the previous render.


Solution

  • You're modifying your array in place if the currency isn't in your array with:

    bankCurrencies.push(currency);
    

    In React, you should treat your state as immutable (i.e., read-only). This means you shouldn't change it in place with methods like .push() or update indexes directly arr[i] = ..., etc. Doing so will cause React to miss that the state value has changed, so it won't rerender. See Updating Arrays in State from the React docs for more info.

    Instead, if you want to update your array, you need to create a new array that contains your modification, one way to do this with your above code is like so:

    const updateActiveCurrencies = (currency) => {
      setActiveCurrencies(activeCurrencies => {
        if (activeCurrencies.includes(currency)) {
          return activeCurrencies.filter((c) => c != currency);
        } else {
          return [...activeCurrencies, currency];
        }
      });
    }
    

    Here we're using [...activeCurrencies to spread (...) the existing values of activeCurrencies into a new array, and then following those elements adding your new currency with , currency]. This also uses the state updater function, where you can instead return the new state value you want to update to.


    Side note: Using .includes() and .filter() is the same as doing a full loop through your array (when .includes() can't find the currency). Consider using an object key-ed by currency and using a flag such as true/false to indicate if the currency is active or not, or use a Set instead which offers better performance when doing lookups compared to arrays.

    eg:

    const [activeCurrencies, setActiveCurrencies] = useState({});
    const updateActiveCurrencies = (currency) => {
      setActiveCurrencies(activeCurrencies => ({
        ...activeCurrencies,
        [currency]: !activeCurrencies[currency]
      }));
    }
    ...
    className={
      activeCurrencies[currency]
        ? "opacity-100"
        : "opacity-50"
    }