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.
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"
}