ExpandPanelRenderer rendering following delete seems to be out of sync

If I have an array of objects, when I delete an object from the middle of my array, all objects with index greater than the object deleted are temporarily wrong. This is best explained with an example. Imagine I have an array of the following:

[
  {firstName: "james", lastName: "morris", dob: "22.05.1990", hair: "brown"},
  {firstName: "john", lastName: "smith", dob: "12.05.1994", hair: "brown"},
  {firstName: "janice", lastName: "jackson", dob: "18.06.1975", hair: "brown"},
  {firstName: "mary", lastName: "shelly", dob: "01.02.1967", hair: "blonde"}
]

and an associated schema. when I pass this schema and data to JSONForms without an explicit UI schema it renders using the MaterialArrayLayoutRenderer. Now when I delete the index=1, john smith entry through the UI, everything initially appears correct. I now have 3 items in my array, with labels, james, janice, mary. However when I expand the janice array object it displays the data from john smith and when I expand the mary, it displays the data from from janice jackson. However I believe that the underlying data is actually fine, it’s just the display of the internal array objects that hasn’t been updated, my reason for thinking this is that I have a home screen in my app that doesn’t contain the JsonForms component and when I switch to and from my homescreen back to the page where I can see the data, everything appears to be in sync and correct.

Initially, I was using a custom MaterialArrayLayoutRenderer, MaterialArrayLayout and ExpandPanelRenderer when I noticed the issue but have since reverted back to using the OOTB renderers and have noticed the same issue.

Hopefully my above description of the issue i’m seeing is sufficient, if not I can provide screenshots and anything else to help debug.

Hi @james-morris, thanks for the detailed report. I’ll look into the problem. Which version of the React Material renderers are you using?

Hi @james-morris, sadly I can’t reproduce the behavior. Can you give some instructions on how to replicate the issue? Ideally as an executable example, e.g. on a JSON Forms React seed fork.

DeleteItemInArrayIssue

I’m using the following:
“@jsonforms/core”: “^2.5.2”,
“@jsonforms/material-renderers”: “^2.5.2”,
“@jsonforms/react”: “^2.5.2”,
“@material-ui/core”: “^4.12.1”,
“@material-ui/icons”: “^4.5.1”,
“@material-ui/lab”: “^4.0.0-alpha.56”,
“@material-ui/pickers”: “^3.2.8”,

I’ll try to create an example for you ASAP. Thanks again for looking into this!

Hi @sdirix, I figured out roughly what’s causing my issue but don’t quite understand why it’s happening. I’ve added a custom MuiInputText component that debounces the input so that the data isn’t validated on every keystroke. Which looks like this:

interface CustomMuiTextInputProps {
    muiInputProps?: InputProps['inputProps'];
    inputComponent?: InputProps['inputComponent'];
}

export const CustomMuiInputText = React.memo((props: CellProps & WithClassname & CustomMuiTextInputProps) => {
    const [showAdornment, setShowAdornment] = useState(false);
    const {
        data,
        config,
        className,
        id,
        enabled,
        uischema,
        isValid,
        path,
        handleChange,
        schema,
        muiInputProps,
        inputComponent
    } = props;
    **const [inputText, setInputText] = useState(data || '')**
    const maxLength = schema.maxLength;
    const appliedUiSchemaOptions = merge({}, config, uischema.options);
    let inputProps: InputBaseComponentProps;
    if (appliedUiSchemaOptions.restrict) {
        inputProps = { maxLength: maxLength };
    } else {
        inputProps = {};
    }

    inputProps = merge(inputProps, muiInputProps);

    if (appliedUiSchemaOptions.trim && maxLength !== undefined) {
        inputProps.size = maxLength;
    }

    **//This was inspired by https://www.freecodecamp.org/news/debounce-and-throttle-in-react-with-hooks/**
**    const debouncedChange = useCallback(**
**        debounce((nextInputText: any)=>{handleChange(path, nextInputText)}, 1000),**
**        []**
**    )**
**    const onChange = (ev: any) => {**
**        setInputText(ev.target.value);**
**        debouncedChange(ev.target.value);**
**    }**

    const theme: JsonFormsTheme = useTheme();
    const inputDeleteBackgroundColor = theme.jsonforms?.input?.delete?.background || theme.palette.background.default;

    return (
        <Input
            type={
                appliedUiSchemaOptions.format === 'password' ? 'password' : 'text'
            }
            **value={inputText}**
            onChange={onChange}
            className={className}
            id={id}
            disabled={!enabled}
            autoFocus={appliedUiSchemaOptions.focus}
            multiline={appliedUiSchemaOptions.multi}
            fullWidth={!appliedUiSchemaOptions.trim || maxLength === undefined}
            inputProps={inputProps}
            error={!isValid}
            onPointerEnter={() => setShowAdornment(true) }
            onPointerLeave={() => setShowAdornment(false) }
            endAdornment={
                <InputAdornment
                    position='end'
                    style={{
                        display:
                            !showAdornment || !enabled || data === undefined ? 'none' : 'flex',
                        position: 'absolute',
                        right: 0
                    }}
                >
                    <IconButton
                        aria-label='Clear input field'
                        onClick={() => handleChange(path, undefined)}
                    >
                        <Close style={{background: inputDeleteBackgroundColor, borderRadius: '50%'}}/>
                    </IconButton>
                </InputAdornment>
            }
            inputComponent={inputComponent}
        />
    );
}, areEqual);

where the bold bits are the bit’s that i’ve changed.
When I delete an object in an array, the data that get’s passed in is updated however it doesn’t update the value of inputText and hence all of the input text values. for my array object are out of date. What am I doing wrong that is causing the inputText not to be updated by the data?

Thanks again

Hi @james-morris, the problem here is that the data coming from JSON Forms is only acknowledged during the initial rendering, i.e.

const [inputText, setInputText] = useState(data || '')

When an array element is deleted, the existing inputs get new data from JSON Forms as they are now used for a different data entry. Your renderer must also check this case, for example via an useEffect, e.g.

useEffect(() => {
  setInputText(data || '');
}, [data]);

Note that since JSON Forms v3.0.0-alpha.2 we already have integrated debounce functionality on text controls, so custom debouncing is no longer needed. If you still need to debounce manually you could use our exported useDebouncedChange, see here.

1 Like