Emad
Emad

Reputation: 366

ReactJs managing an array of stateful components

I have a parent component that I want to use to dynamically add and remove an stateful component, but something weird is happening.

This is my parent component and how I control my list of FilterBlock

import React from 'react'
import FilterBlock from './components/FilterBlock'

export default function List() {
    const [filterBlockList, setFilterList] = useState([FilterBlock])
    
    const addFilterBlock = () => {
        setFilterList([...filterBlockList, FilterBlock])
    }

    const onDeleteFilterBlock = (index) => {
        const filteredList = filterBlockList.filter((block, i) => i !== index);
        setFilterList(filteredList)
    }

    return (
        <div>
            {
                filterBlockList.map((FilterBlock, index) => (
                    <FilterBlock
                        key={index}
                        onDeleteFilter={() => onDeleteFilterBlock(index)}
                    />
                ))
            }
            <button onClick={addFilterBlock}></button>
        </div>
    )
}

You can assume FilterBlock is a stateful react hooks component.

My issue is whenever I trigger the onDeleteFilter from inside any of the added components, only the last pushed FilterBlock gets removed from the list even though I remove it based on its index on the list.

Please check my example here:

https://jsfiddle.net/emad2710/wzh5Lqj9/10/

Upvotes: 3

Views: 311

Answers (1)

glinda93
glinda93

Reputation: 8489

The problem is you're storing component function FilterBlock to the state and mutate the parent state inside a child component.

You may know that mutating state directly will cause unexpected bugs. And you're mutating states here:

/* Child component */

function FilterBlock(props) {
    const [value, setValue] = React.useState('')
    const options = [
        { value: 'chocolate', label: 'Chocolate' },
        { value: 'strawberry', label: 'Strawberry' },
        { value: 'vanilla', label: 'Vanilla' }
    ]
    const onInputChange = (e) => {
        // child state has been changed, but parent state
        // does not know about it
        // thus, parent state has been mutated
        setValue(e.target.value)
    }
    
    return (
        <div>
            <button onClick={props.onDeleteFilter}>remove</button>
            <input value={value} onChange={onInputChange}></input>
              
        </div>
    )
}

/* Parent component */

const addFilterBlock = () => {
  setFilterList([...filterBlockList, FilterBlock])
}

filterBlockList.map((FilterBlock, index) => (
  <FilterBlock
    key={index}
    onDeleteFilter={() => onDeleteFilterBlock(index)}
  />
))

When a FilterBlock value changes, FilterBlock will store its state somewhere List component cannot control. That means filterBlockList state has been mutated. This leads to unpredictable bugs like the above problem.

To overcome this problem, you need to manage the whole state in parent component:

const {useCallback, useState} = React;


function FilterBlock(props) {

    const options = [
        { value: 'chocolate', label: 'Chocolate' },
        { value: 'strawberry', label: 'Strawberry' },
        { value: 'vanilla', label: 'Vanilla' }
    ]

    return (
        <div>
            <button onClick={props.onDeleteFilter}>remove</button>
            <input value={props.value} onChange={props.onChange}></input>
              
        </div>
    )
}


function App() {
    
    const [values, setValues] = useState(['strawberry']);
    
    const addFilterBlock = useCallback(() => {
            setValues(values.concat(''));
    }, [values]);

    const onDeleteFilterBlock = useCallback((index) => {
        setValues(values.filter((v, i) => i !== index));
    }, [values]);
    
    const setFilterValue = useCallback((index, value) => {
        const newValues = [...values];
        newValues[index] = value;
        setValues(newValues);
    }, [values]);

    return (
        <div>
            {
                values.map((value, index) => (
                    <FilterBlock
                        key={index}
                        onDeleteFilter={() => onDeleteFilterBlock(index)}
                        value={value}
                        onChange={(e) => setFilterValue(index, e.target.value)}
                    />
                ))
            }
            <button onClick={addFilterBlock}>add</button>
        </div>
    )
}


ReactDOM.render(<App />, document.querySelector("#app"))

JSFiddle

Upvotes: 1

Related Questions