React not re-rendering after array state update
Asked Answered
T

3

18

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?

This is the current state

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 
Trinia answered 11/9, 2019 at 10:20 Comment(1)
Looks like you are update the mutating the state here listFeatures[ idx ].checked = e.target.checked and returning the same object, use spread (...) operator and set state like setListFeatures( [...newFeatures] ) this will return new object which should trigger re-render.Enchilada
M
26

The problem is in updateFeaturesList, you are mutating the state directly in this line listFeatures[ idx ].checked = e.target.checked, the state reference stays 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;
    console.log( "Updated features list", newFeatures, e.target.checked );
    setListFeatures( newFeatures );
}
Mercaptide answered 11/9, 2019 at 10:27 Comment(1)
I came from an Angular background, and this is so frustrating. Thank you for the clean answer !@Mohamed RamramiIslet
T
2

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.

Tiu answered 11/9, 2019 at 10:26 Comment(0)
E
1

React state will trigger render only if value changed in state. Modifying or pushing values to array wont change the array reference, here react state uses array reference to decide whether to trigger render or not.so here array is modifying but reference is not changing

solution: copy your array to new array and set to state will solve issue

Epergne answered 18/1, 2022 at 18:32 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Codel

© 2022 - 2024 — McMap. All rights reserved.