React not re-rendering after array state update
I have a checkbox list UI that is rendered from an array. After I update the array the checkbox list state does not update.
I moved the code where the list is mapped but it does not change the results. The DOM re-render does not happen, see gif below.
I have been looking arround and I see that this issue is already reported however the solution about moving the list.map
code out of the function it did not work for me.
Could you suggest me a solution? What is the source of this problem?
import React,{ useState } from "react"
import
{
Box,
DropButton,
Grid,
Text,
Calendar,
RangeInput,
CheckBox
} from "grommet"
const CalButton = ( { label,onSelect } ) =>
{
return (
<DropButton
label={ label }
dropAlign={ { top: 'bottom',left: 'left' } }
margin="small"
dropContent={
<Box pad="medium" background="ligth-3" elevation="small">
<Calendar range size="medium" onSelect={ onSelect } />
</Box>
} />
)
}
const RangeButton = ( { label,value,onChange,min,max,step,unit,header } ) =>
{
return (
<DropButton
label={ value === null ? label : value + ' ' + unit }
dropAlign={ { top: 'bottom',left: 'left' } }
margin="small"
dropContent={
<Box pad="medium"
background="ligth-3"
elevation="small"
align="center"
>
<Text size="small">{ header }</Text>
<RangeInput
value={ value }
min={ min } max={ max }
onChange={ onChange }
step={ step }
/>
<Text weight="bold">{ value }</Text>
<Text weight="normal">{ unit }</Text>
</Box>
} />
)
}
const FeaturesButton = ( { label,features,onChange } ) =>
{
const FeaturesList = ( { features,onChange } ) => (
<>
{ features.map( ( item,idx ) => (
<CheckBox
key={ item.name }
label={ item.name }
checked={ item.checked }
onChange={ e => onChange( e,idx ) } />)
)
}
</>
)
return (
<DropButton
label={ label }
dropAlign={ { top: 'bottom',left: 'left' } }
margin="small"
dropContent={
<Box pad="medium"
background="ligth-3"
elevation="small"
align="start"
direction="column"
gap="small"
>
<FeaturesList
features={features}
onChange={onChange} />
</Box>
} />
)
}
const destApp = () =>
{
const [ windStrength,setWindStrength ] = useState( null )
const [ windFrequency,setWindFrequency ] = useState( null )
const [ cost,setCost ] = useState( null )
const date = new Date()
const [ month,setMonth ] = useState( date.getMonth() )
const [ listFeatures,setListFeatures ] = useState( [
{
name: 'Butter flat water',
checked: false,
},
{
name: 'Moderately flat water',
checked: false,
},
{
name: '1-2m Waves',
checked: false,
},
{
name: '2m+ Waves',
checked: false,
},
{
name: 'Beginer Friendly',
checked: false,
},
{
name: 'Kite-in-kite-out',
checked: false,
},
{
name: 'Nightlife',
checked: false,
}
] )
const months = [ 'January','February','March','April','May','June','July','August','September','October','November','December' ];
const updateFeaturesList = ( e,idx ) =>
{
listFeatures[ idx ].checked = e.target.checked
const newFeatures = listFeatures
setListFeatures( newFeatures )
console.log( "Updated features list",newFeatures,e.target.checked )
}
return (
<Grid rows={ [ "xsmall","fill" ] }
areas={ [ [ "filterBar" ],[ "results" ] ] }
gap="xxsmall">
<Box gridArea="filterBar"
direction="row-responsive"
gap="xsmall"
pad="xsmall"
justify="center" >
<CalButton label={ months[ month ].toLowerCase() } onSelect={ ( data ) => console.log( data ) } />
<RangeButton
label="wind strength"
header="What's your wind preference?"
min="15"
max="45"
unit="kts"
value={ windStrength }
step={ 1 }
onChange={ ( e ) =>
{
setWindStrength( e.target.value )
console.log( windStrength )
} } />
<RangeButton
label="wind frequency"
header="How often does your destination need to be windy?"
min="1"
max="7"
unit="days/week"
value={ windFrequency }
step={ 1 }
onChange={ ( e ) =>
{
setWindFrequency( e.target.value )
console.log( windFrequency )
} } />
<RangeButton
label="cost"
header="Average daily cost: 1 lunch, diner and doubble room at a midrange hotel?"
min="10"
max="400"
unit="€"
value={ cost }
step={ 1 }
onChange={ ( e ) =>
{
setCost( e.target.value )
console.log( cost )
} } />
<FeaturesButton
label="important features "
features={ listFeatures }
onChange={ updateFeaturesList }
/>
</Box>
<Box gridArea="results"
margin="">
Results go in here!
</Box>
</Grid>
)
}
export default destApp
The problem is in updateFeaturesList
, you are mutating the state directly in this line listFeatures[ idx ].checked = e.target.checked
, the state reference stay the same and so react does not know if it should re-render.
What you can do is copy the state, before changing it :
const updateFeaturesList = ( e,idx ) =>
{
const newFeatures = [...listFeatures];
newFeatures[ idx ].checked = e.target.checked
setListFeatures( newFeatures )
console.log( "Updated features list",newFeatures,e.target.checked )
}
You're mutating the original state in your updateFeaturesList
function. Use the functional form of setState
to update your current feature list:
const updateFeaturesList = (e, idx) => {
const { checked } = e.target;
setListFeatures(features => {
return features.map((feature, index) => {
if (id === index) {
feature = { ...feature, checked };
}
return feature;
});
});
};
Also note that calling console.log("Updated features list", newFeatures,e.target.checked)
immediately after setting the state won't show the updated state, since setting state is async.