'React: Updating shared state from one child component has unwanted side effects on other

I'm trying to make a piece of code to allow dynamic rule-making for Trading strategies. When it comes to selecting Indicators to use, I've bumped into a bit of a problem. You don't need to understand anything about Trading to help me out with this. (But to be honest, I know nothing either)

I have the following component, IndicatorSelector, which is the parent component.

import { Grid, Button, Stack, Typography } from '@mui/material';
import IndicatorBox from './indicatorBox';


export default function IndicatorSelector(props) {
    const [chosenIndicators, setChosenIndicators] = useState([]);
    
    const availableIndicators = // LOAD THESE FROM LOCAL FILE, EXAMPLE GIVEN BELOW.

    const addIndicator = (val) => {
        const temp = [...chosenIndicators];
        temp.push(val);
        setChosenIndicators(temp);
    }
    
    const updateOptions = (i, key, value) => {
        const newArray = [...chosenIndicators];
        newArray[i].arguments[key] = value;    
        setChosenIndicators(newArray);
    }

    const chooseIndicator = (i, name) => {
        availableIndicators.forEach((val, index) => {
            if (val.name === name) {
                const temp = [...chosenIndicators];
                temp[i] = val;
                console.log("Choosing " + val.name);
                console.log(temp);
                setChosenIndicators(temp);
            }
        })
    }

    const deleteIndicator = (i) => {
        const temp = [...chosenIndicators];
        temp.splice(i, 1);
        setChosenIndicators(temp);
    }

    const handleSubmit = () => {
        if (chosenIndicators.length < 1) {
            alert("Please select an indicator!");
        } else {
            props.onComplete(chosenIndicators);
        }
    }

    return (
        <Stack>
            <Typography variant="h3" sx={{paddingBottom: 5}}>Step 1: Choose Your Indicators</Typography>
            <Grid item xs={12}>
                <Stack>
                    {chosenIndicators.map(
                        (indicator, i) => {
                            return <IndicatorBox key={i} indicator={indicator} chooseIndicator={chooseIndicator} updateOptions={updateOptions} onDelete={deleteIndicator} index={i} available={availableIndicators}></IndicatorBox>
                        }
                    )}
                </Stack>
            </Grid>
            <Grid item xs={12} sx={{display: 'flex', justifyContent: 'space-evenly'}}>
                <Button variant="contained" color="secondary" onClick={() => {addIndicator(availableIndicators[0])}}>Add Another</Button>
                <Button variant="contained" color="success" onClick={handleSubmit}>Submit</Button>
            </Grid>
        </Stack>
    )  
}

This component displays some buttons to allow you to add more indicators and to submit your choices. Then, the options for selecting indicators are displayed as multiple IndicatorBox components. This component is shown below.

import { Grid, Select, FormControl, InputLabel, MenuItem, TextField, Box, IconButton, Typography } from '@mui/material';
import ClearIcon from '@mui/icons-material/Clear';

export default function IndicatorBox(props) {

    const choose = (name) => {
        props.chooseIndicator(props.index, name);
    }

    return (
        <Box sx={{bgcolor: '#212121', padding: 1, borderRadius: 2, width: '90vw', diplay: 'flex', justifyContent: 'space-evenly'}}>
            <Grid container>
                <Typography variant="body2">{props.index}</Typography>
                <Grid item xs={11}>
                <FormControl sx={{width: '95%'}}>
                    <InputLabel id="indicator-selector">Indicator</InputLabel>
                    <Select labelId="indicator-selector"
                        value={props.indicator.name}
                        label="Indicator"
                        onChange={(e) => {choose(e.target.value)}}
                    >
                        {props.available.map((object, i) => {
                            return <MenuItem key={object.name} value={object.name}>{object.properName}</MenuItem>
                        })}
                    </Select>
                </FormControl>
                </Grid>
                <Grid item sx={{padding: 1}}>
                    <IconButton onClick={() => props.onDelete(props.index)}>
                        <ClearIcon />
                    </IconButton>
                </Grid>
                { (Object.keys(props.indicator).length > 0) ? 
                    <Grid item xs={12} sx={{paddingTop: 1.5}}>
                        {Object.keys(props.indicator.arguments).map((key, i) => {
                            return <TextField
                                key={key}
                                label={key.toUpperCase()}
                                value={props.indicator.arguments[key] ? props.indicator.arguments[key] : 0}
                                variant="outlined"
                                onChange={(e) => props.updateOptions(props.index, key, parseInt(e.target.value))}
                            />
                        })}
                    </Grid>
                 : null}
            </Grid>
        </Box>
    )  
}

To add further detail, an example of the JSON structure of an 'indicator' is shown below.

[
    {
        "name": "rsi",
        "properName": "Relative Strength Index",
        "functionName": "talib.RSI",
        "plot": null,
        "data": ["close"],
        "arguments": {
            "timeperiod": 14
        },
        "output": [
            "rsi"
        ]
    }
]

When running the code, one is presented with this screen.

When a user clicks "add another", then selects an indicator, the arguments for said indicator pop up underneath it, like so.

Now, here's where the problem arises. If the user selects a second indicator of the same type, the options values become linked, with both of them changing when the user types in either text field. This occurs no matter how many indicators are selected, as long as two or more are of the same type.

The desired outcome: I want the IndicatorBoxes to have their values be independent of one another, and for the array in IndicatorSelector: 'chosenIndicators', to reflect their true, independent values. I want the user to be able to add as many indicators as they want to the array, with each of them having their own independently customisable options.

EDIT: To clarify, 'availableIndicators' is not a list that is meant to change during execution of the program. Think of it as a 'menu' of indicators that the user can select from. When they choose one, the indicator is found by name from 'availableIndicators', and copied into the array 'chosenIndicators'. I don't think this should affect availableIndicators, but for some reason, chosen indicators seem to be linked when two or more of the same are selected.

This issue has likely been caused by my lack of experience and knowledge with React, so you're more than welcome to rag on my appalling code. In fact, any advice on how to approach this problem better would be appreciated.



Solution 1:[1]

This is not really a fulll answer but it way to long for a comment. If I am following your code correctly, it looks like you are filtering the inidicators here:

const chooseIndicator = (i, name) => {
        availableIndicators.forEach((val, index) => {
            if (val.name === name) {
                const temp = [...chosenIndicators];
                temp[i] = val;
                console.log("Choosing " + val.name);
                console.log(temp);
                setChosenIndicators(temp);
            }
        })
    }

So if 2 or more indicators have the same name (you say type I assume you mean name?), Your filter condition is going to match both of them.

Instead of filtering by name you should inject a unique id onto the nested object using a library like uuid that way you can filter by something that you know will be unique .

So when you an indicator is created you would do something like:

indicatorData.id = uuid()

before setting to state

now you can filter through them by something you know is unique.

Also, there is array.find and array.filter methods that are specifically meant for iterating over an array and returning elements based on a condition, no need to do array.forEach followed by an if-else

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Shaynel