React: Updating nested arrays?

I have a nested array like

"datasets":{
   "type":"array",
   "default":[
     {
       "name":"Main",
       "data":[
         {
           "Department":"Sheriff",
           "Budget":100000,
           "MeetAt":"2025-01-26T14:30:00Z",
           "preferredColor":"red",
           "PostContent":"<div> </div>"
         },
         {
           "Department":"Assessor",
           "Budget":20000,
           "MeetAt":"2025-01-26T14:30:00Z",
           "preferredColor":"#232323",
           "PostContent":"<div> </div>"
         },
         {
           "Department":"Treasurer",
           "Budget":30000,
           "MeetAt":"2025-01-26T14:30:00Z",
           "preferredColor":"#E72323",
           "PostContent":"<div> </div>"
         }
       ]
     },
     {
       "name":"Locations",
       "data":[
         {
           "State":"California",
           "Coordinates":"",
           "MeetAt":"2025-01-26T14:30:00Z",
           "PreferredColor":"#e0e0e0",
           "PostContent":"<div> </div>"
         },
         {
           "State":"Texas",
           "Coordinates":"",
           "MeetAt":"2025-01-26T14:30:00Z",
           "PreferredColor":"#e0e0e0",
           "PostContent":"<div> </div>"
         },
         {
           "State":"Florida",
           "Coordinates":"",
           "MeetAt":"2025-01-26T14:30:00Z",
           "PreferredColor":"#e0e0e0",
           "PostContent":"<div> </div>"
         }
       ]
     }
   ]
},

And then I have a component that renders child components called with one being rendered for each object inside the ‘datasets’.

import { useState, useEffect } from '@wordpress/element';
import { Button, Modal } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import LCPDataGrid from './LCPDataGrid';

const LCPDatasetBuilder = ({ attributes, setAttributes }) => {
    const [isOpen, setIsOpen] = useState(false);
    const [activeTab, setActiveTab] = useState(0); // Track the active tab

    const handleUpdateDataset = (index, newData) => {
        console.log("newData (formatted for copy-pasting):", JSON.stringify(newData, null, 2));
    
        // Check if newData.data is an array
        if (Array.isArray(newData.data)) {
            // Create a deep copy of the datasets to avoid mutating the state directly
            const datasets = [...attributes.datasets];
    
            // Check if the dataset at the specified index exists
            if (datasets[index]) {
                // Create a deep copy of the dataset at the given index
                const updatedDataset = {
                    ...datasets[index],  // Copy the dataset object
                    data: [...newData.data],  // Deep copy the data array
                };
    
                // Update the dataset at the given index
                datasets[index] = updatedDataset;
    
                // Create a new object for all attributes (to force re-render)
                setAttributes({
                    ...attributes,  // Copy the existing attributes
                    datasets: datasets, // Replace the datasets with the updated one
                });
    
                console.log("Datasets after update:", JSON.stringify(datasets, null, 2));
            } else {
                console.error("Dataset not found at index:", index);
            }
        } else {
            console.error("Invalid data format in newData:", newData);
        }
    };
    

    useEffect(() => {
        // If needed, you can add logic to do something when datasets change
        console.log('Datasets have been updated:', attributes.datasets);
    }, [attributes.datasets]);

    return (
        <>
            <Button
                variant="secondary"
                onClick={() => setIsOpen(true)}
                style={{ marginBottom: '10px', width: '100%' }}
            >
                {__('Edit Dataset', 'lcp-visualize')}
            </Button>

            {isOpen && (
                <Modal
                    onRequestClose={() => setIsOpen(false)}
                    title={__('Dataset Builder', 'lcp-visualize')}
                    style={{ width: '90vw', height: '90vh' }}
                >
                    <div style={{ height: 'calc(90vh - 40px)', padding: '20px' }}>
                        {/* Tabs */}
                        <div style={{ display: 'flex', marginBottom: '20px' }}>
                            {attributes.datasets.map((dataset, index) => (
                                <button
                                    key={index}
                                    onClick={() => setActiveTab(index)} // Set the active tab
                                    style={{
                                        padding: '10px 20px',
                                        margin: '0 5px',
                                        backgroundColor: activeTab === index ? '#007cba' : '#f1f1f1',
                                        color: activeTab === index ? 'white' : 'black',
                                        border: '1px solid #ccc',
                                        borderRadius: '5px',
                                        cursor: 'pointer',
                                        transition: 'background-color 0.3s ease',
                                    }}
                                >
                                    {dataset.name}
                                </button>
                            ))}
                        </div>

                        {/* Render all the LCPDataGrid components */}
                        <div style={{ display: 'flex', flexDirection: 'column' }}>
                            {attributes.datasets.map((dataset, index) => (
                                <div
                                    key={index}
                                    style={{
                                        display: activeTab === index ? 'block' : 'none', // Show only the active tab
                                        transition: 'display 0.3s ease',
                                    }}
                                >
                                    {/* Pass key prop to trigger re-render if necessary */}
                                    <LCPDataGrid
                                        key={index}  // Ensures each grid instance is unique
                                        dataset={dataset.data}  // Pass the specific dataset data
                                        index={index}
                                        updateDataset={handleUpdateDataset} // Function to handle updates
                                    />
                                </div>
                            ))}
                        </div>
                    </div>
                </Modal>
            )}
        </>
    );
};

export default LCPDatasetBuilder;

The LCPDataGrid.js component passes back the updated data as well as the index (for the respective data object to update). I’m having problems getting the changes to actually be saved in this part of the code

const handleUpdateDataset = (index, newData) => {
	console.log("newData (formatted for copy-pasting):", JSON.stringify(newData, null, 2));

	// Check if newData.data is an array
	if (Array.isArray(newData.data)) {
		// Create a deep copy of the datasets to avoid mutating the state directly
		const datasets = [...attributes.datasets];

		// Check if the dataset at the specified index exists
		if (datasets[index]) {
			// Create a deep copy of the dataset at the given index
			const updatedDataset = {
				...datasets[index],  // Copy the dataset object
				data: [...newData.data],  // Deep copy the data array
			};

			// Update the dataset at the given index
			datasets[index] = updatedDataset;

			// Create a new object for all attributes (to force re-render)
			setAttributes({
				...attributes,  // Copy the existing attributes
				datasets: datasets, // Replace the datasets with the updated one
			});

			console.log("Datasets after update:", JSON.stringify(datasets, null, 2));
		} else {
			console.error("Dataset not found at index:", index);
		}
	} else {
		console.error("Invalid data format in newData:", newData);
	}
};

I suspect it’s because React doesn’t recognize this as an actual change in the data…

If I replace

setAttributes({
	...attributes,  // Copy the existing attributes
	datasets: datasets, // Replace the datasets with the updated one
});

with a literal datasets array like

setAttributes({
	...attributes,  // Copy the existing attributes
	datasets: [{...new data }], // Replace the datasets with the updated one
});

then it updates as expected.

Unfortunately I don’t know React well enough to give a solution, but there are a few things that look a bit odd to me.

You say you have a nested array, are you referring to the default array? datasets is a property with an object as it’s value, not an array.

"datasets":{
   "type":"array",
   "default":[
     {
       "name":"Main",
       "data":[
         {
           "Department":"Sheriff",
           "Budget":100000,
           "MeetAt":"2025-01-26T14:30:00Z",
           "preferredColor":"red",
           "PostContent":"<div> </div>"
         },
         {
           "Department":"Assessor",
           "Budget":20000,
           "MeetAt":"2025-01-26T14:30:00Z",
           "preferredColor":"#232323",
           "PostContent":"<div> </div>"
         },
         {
           "Department":"Treasurer",
           "Budget":30000,
           "MeetAt":"2025-01-26T14:30:00Z",
           "preferredColor":"#E72323",
           "PostContent":"<div> </div>"
         }
       ]
     },
     ...
   ]
},

Is the above an example of attributes.datasets? e.g. is datasets above a property of attributes?

If so I am a bit confused how you would be accessing datasets by index.

if (datasets[index]) {
    const updatedDataset = {
        ...datasets[index],  // Copy the dataset object
        data: [...newData.data],  // Deep copy the data array
    };

A key maybe e.g. type or default. Or if by index I could understand you accessing the child default property that way e.g.

if (datasets.default[index]) {
    const updatedDataset = {
        ...datasets.default[index],  // Copy the dataset object
        data: [...newData.data],  // Deep copy the data array
    };

I mean later on in your script in the Modal component you are calling the map method on datasets as if it is an array.

attributes.datasets.map((dataset, index) => ( ....

Maybe it’s my ignorance when it comes to React, but it’s just a bit hard to follow as the datasets example doesn’t seem to tally with this.

Oh and just for clarity the ...spread operator creates a shallow copy, not a deep copy.

If you do actually want a deep copy you can use JSON.stringify and JSON.parse or Lodash’s _.cloneDeep or there is a built-in which I haven’t really tried out thoroughly structuredClone

1 Like