Array in React State is overwritten when handling mulitple files - reactjs

I have a system where, once the user selects an Excel file, the file is read into state MultipleFileTableData.ExcelData and is then shown using AntD table.
When the user selects a file, the file is uploaded to an API which returns the original filename and the filename as stored in the system.
Once the file is uploaded, the data is then updated to indicate that the file has uploaded and the filename as stored in the system is added.
The above works perfectly fine if a single file is selected, however the system needs to allow multiple files. As soon as you select more than one file to upload, the state is overwritten. In short, the first file row is updated with the flag for file uploaded and the stored filename. But then once the second file is processed, the first file reverts.
I've omitted most of the code as I beleive it is not relevant. Code below:
const CertificateUploadMultiple = ({FormState, SetFormState}) => {
const { Dragger } = Upload;
const [MultipleFileTableData, SetMultipleFileTableData] = useState({ExcelData: []});// userRef([]); // This holds the data from the imported excel file
const BatchUploadTableColumns = [
{title: 'Customer', dataIndex: 'Customer Name', key: 'Customer Name'},
{title: 'Serial Number', dataIndex: 'Serial Number', key: 'Serial Number'},
{title: 'Certificate Number', dataIndex: 'Certificate Number', key: 'Certificate Number'},
{title: 'Certificate Date', dataIndex: 'Certificate Date', key: 'Certificate Date'},
{title: 'Certificate File', dataIndex: 'Certificate Filename', key: 'Certificate Filename'},
{title: 'Certificate Uploaded', align: 'right', render: (text, record, index) => {
return (<>
{record['FileUploaded'] == 'Y' ? <span>File uploaded</span> : <span>Awaiting File</span>}
<Button danger size='small' style={{marginLeft: "0.5em"}} onClick={() => {DeleteExcelRowData(text, record, index);}}><DeleteOutlined /></Button>
</>)
}
},
]
const ReadTemplateFile = async (e) => {
console.log("ReadTemplateFile", e);
const AllowedFiles = ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'];
if (!AllowedFiles.includes(e.type)) {
let Content = <><p>'{file.name}' is not a permitted file</p><p>Received File type: '{file.type}'</p></>;
Modal.error({
title: 'Invalid File',
content: Content
})
return false;
}
const reader = new FileReader();
reader.onload = (evt) => {
const bstr = evt.target.result;
const wb = XLSX.read(bstr, { type: "binary" });
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
const RawData = XLSX.utils.sheet_to_json(ws);
console.log("Excel Raw Data", RawData);
const Rows = RawData.map((row, idx) => ({...row, key:idx, FileUploaded: 'N', UploadedFileName: ''}));
SetMultipleFileState(prevState => ({...prevState, ShowTemplateUpload: false}));
SetMultipleFileTableData(prevState => ({...prevState, ExcelData: Rows}));
};
reader.readAsBinaryString(e);
return false;
}
const RowColour = (record) => {
//if (UploadedFileList.includes(record['Certificate Filename'])){
//if (UploadedFileListRef.current.includes(record['Certificate Filename'])){
/*if (record.FileUploaded == 'Y'){
return 'fileuploaded';
}*/
return record.FileUploaded === 'Y' ? 'fileuploaded' : 'awaitingfile';
//return 'awaitingfile';
}
const MultipleCertificateFileUploadProcessFile = async (options) => {
const { onSuccess, onError, file, onProgress } = options;
const UploadProgressHandler = (event) => {
const percent = Math.floor((event.loaded / event.total) * 100);
setProgress(percent);
if (percent === 100) {
setTimeout(() => setProgress(0), 1000);
}
onProgress({ percent: (event.loaded / event.total) * 100 });
}
const Data = new FormData();
Data.append("CertificateFile", file);
try {
CertificateService.UploadCertificateFile(Data, UploadProgressHandler)
.then((resp) => {
if (resp.data.code == '200'){
// mark the line as completed
/*let idx = MultipleFileTableData.ExcelData.findIndex((obj => obj["Certificate Filename"] === resp.data.UploadedOriginalFileName));
console.log("idx", idx);
let t = MultipleFileTableData.ExcelData[idx];
t.FileUploaded = 'Y';
t.UploadedFileName = resp.data.UploadedFileName;*/
//SetMultipleFileTableData(prevState => ({...prevState, ExcelData: [...prevState.ExcelData, t]}));
let t = MultipleFileTableData.ExcelData.map(p => p["Certificate Filename"] === resp.data.UploadedOriginalFileName ?
{...p, FileUploaded: 'Y', UploadedFileName: resp.data.UploadedFileName}
: p
);
SetMultipleFileTableData(prevState => ({...prevState, ExcelData: t}));
onSuccess("Ok");
} else {
message.error(<div>Unable to process file '{resp.data.CertificateFileName}': {resp.data.msg}</div>)
console.log("Error - ", resp.data);
onError(resp.data);
}
console.log("resp", resp);
});
} catch (err) {
console.log( err);
onError(err);
}
}
Here is the visual part
<Row>
<Col span={20}>
<Table
// dataSource={MultipleFileState.ExcelFileData}
dataSource={MultipleFileTableData.ExcelData}
columns={BatchUploadTableColumns}
rowClassName={RowColour}
pagination={false}
/>
</Col>
<Col span={4}>
<Dragger
name='BatchCertificateFile'
multiple={true}
beforeUpload={MultipleCertificateFileBeforeUpload}
style={{marginLeft: "1em"}}
onChange={MultipleCertificateFileUploadOnChange}
customRequest={MultipleCertificateFileUploadProcessFile}
showUploadList={true}
fileList={MultipleFileState.MultipleCertificateFileList}
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">Click or drag certificate files to this area to start processing</p>
</Dragger>
</Col>
</Row>

OK so, to prevent this I needed to stop mutating the state with the current state value. This is what was causing the value to be overwritten. Turns out that I should have moved the MultipleFileTableData.ExcelData.map to within the SetMultipleFileTableData and instead use the prevState variable.
SetMultipleFileTableData(prevState => ({...prevState, ExcelData:
(
prevState.ExcelData.map(p => p["Certificate Filename"] === resp.data.UploadedOriginalFileName ?
{...p, FileUploaded: 'Y', UploadedFileName: resp.data.UploadedFileName}
: p
)
)
}));

Related

REACT- Displaying and filtering specific data

I want to display by default only data where the status are Pending and Not started. For now, all data are displayed in my Table with
these status: Good,Pending, Not started (see the picture).
But I also want to have the possibility to see the Good status either by creating next to the Apply button a toggle switch : Show good menus, ( I've made a function Toggle.jsx), which will offer the possibility to see all status included Good.
I really don't know how to do that, here what I have now :
export default function MenuDisplay() {
const { menuId } = useParams();
const [selected, setSelected] = useState({});
const [hidden, setHidden] = useState({});
const [menus, setMenus] = useState([]);
useEffect(() => {
axios.post(url,{menuId:parseInt(menuId)})
.then(res => {
console.log(res)
setMenus(res.data.menus)
})
.catch(err => {
console.log(err)
})
}, [menuId]);
// If any row is selected, the button should be in the Apply state
// else it should be in the Cancel state
const buttonMode = Object.values(selected).some((isSelected) => isSelected)
? "apply"
: "cancel";
const rowSelectHandler = (id) => (checked) => {
setSelected((selected) => ({
...selected,
[id]: checked
}));
};
const handleClick = () => {
if (buttonMode === "apply") {
// Hide currently selected items
const currentlySelected = {};
Object.entries(selected).forEach(([id, isSelected]) => {
if (isSelected) {
currentlySelected[id] = isSelected;
}
});
setHidden({ ...hidden, ...currentlySelected });
// Clear all selection
const newSelected = {};
Object.keys(selected).forEach((id) => {
newSelected[id] = false;
});
setSelected(newSelected);
} else {
// Select all currently hidden items
const currentlyHidden = {};
Object.entries(hidden).forEach(([id, isHidden]) => {
if (isHidden) {
currentlyHidden[id] = isHidden;
}
});
setSelected({ ...selected, ...currentlyHidden });
// Clear all hidden items
const newHidden = {};
Object.keys(hidden).forEach((id) => {
newHidden[id] = false;
});
setHidden(newHidden);
}
};
const matchData = (
menus.filter(({ _id }) => {
return !hidden[_id];
});
const getRowProps = (row) => {
return {
style: {
backgroundColor: selected[row.values.id] ? "lightgrey" : "white"
}
};
};
const data = [
{
Header: "id",
accessor: (row) => row._id
},
{
Header: "Name",
accessor: (row) => (
<Link to={{ pathname: `/menu/${menuId}/${row._id}` }}>{row.name}</Link>
)
},
{
Header: "Description",
//check current row is in hidden rows or not
accessor: (row) => row.description
},
{
Header: "Status",
accessor: (row) => row.status
},
{
Header: "Dishes",
//check current row is in hidden rows or not
accessor: (row) => row.dishes,
id: "dishes",
Cell: ({ value }) => value && Object.values(value[0]).join(", ")
},
{
Header: "Show",
accessor: (row) => (
<Toggle
value={selected[row._id]}
onChange={rowSelectHandler(row._id)}
/>
)
}
];
const initialState = {
sortBy: [
{ desc: false, id: "id" },
{ desc: false, id: "description" }
],
hiddenColumns: ["dishes", "id"]
};
return (
<div>
<button type="button" onClick={handleClick}>
{buttonMode === "cancel" ? "Cancel" : "Apply"}
</button>
<Table
data={matchData}
columns={data}
initialState={initialState}
withCellBorder
withRowBorder
withSorting
withPagination
rowProps={getRowProps}
/>
</div>
);
}
Here my json from my api for menuId:1:
[
{
"menuId": 1,
"_id": "123ml66",
"name": "Pea Soup",
"description": "Creamy pea soup topped with melted cheese and sourdough croutons.",
"dishes": [
{
"meat": "N/A",
"vegetables": "pea"
}
],
"taste": "Good",
"comments": "3/4",
"price": "Low",
"availability": 0,
"trust": 1,
"status": "Pending",
"apply": 1
},
//...other data
]
Here my CodeSandbox
Here a picture to get the idea:
Here's the second solution I proposed in the comment:
// Setting up toggle button state
const [showGood, setShowGood] = useState(false);
const [menus, setMenus] = useState([]);
// Simulate fetch data from API
useEffect(() => {
async function fetchData() {
// After fetching data with axios or fetch api
// We process the data
const goodMenus = dataFromAPI.filter((i) => i.taste === "Good");
const restOfMenus = dataFromAPI.filter((i) => i.taste !== "Good");
// Combine two arrays into one using spread operator
// Put the good ones to the front of the array
setMenus([...goodMenus, ...restOfMenus]);
}
fetchData();
}, []);
return (
<div>
// Create a checkbox (you can change it to a toggle button)
<input type="checkbox" onChange={() => setShowGood(!showGood)} />
// Conditionally pass in menu data based on the value of toggle button "showGood"
<Table
data={showGood ? menus : menus.filter((i) => i.taste !== "Good")}
/>
</div>
);
On ternary operator and filter function:
showGood ? menus : menus.filter((i) => i.taste !== "Good")
If button is checked, then showGood's value is true, and all data is passed down to the table, but the good ones will be displayed first, since we have processed it right after the data is fetched, otherwise, the menus that doesn't have good status is shown to the UI.
See sandbox for the simple demo.

prevent api from being called before state is updated

I have a list of objects. I want to make an api call once the location field of the object is changed. So for that, I have a useEffect that has id, index and location as its dependencies. I have set a null check for the location, if the location isn't empty, I want to make the api call. But the thing is, the api is being called even when the location is empty, and I end up getting a 400. How can I fix this and make the call once location isn't empty?
const [plants, setPlants] = useState([
{
name: 'Plant 1',
id: uuidv4(),
location: '',
coords: {},
country: '',
isOffshore: false,
}
]);
const [locationIDObject, setlocationIDObject] = useState({
id: plants[0].id,
index: 0
});
const handlePlantLocChange = (id, index, value) => {
setPlants(
plants.map(plant =>
plant.id === id
? {...plant, location: value}
: plant
)
)
setlocationIDObject({
id: id,
index: index
})
}
const getCoords = (id, index) => {
axios.get('http://localhost:3002/api/coords', {
params: {
locAddress: plants[index].location
}
}).then((response) => {
if(response.status === 200) {
handlePlantInfoChange(id, PlantInfo.COORD, response.data)
}
})
}
const handler = useCallback(debounce(getCoords, 5000), []);
useDeepCompareEffect(() => {
if(plants[locationIDObject.index].location !== '')
handler(locationIDObject.id, locationIDObject.index);
}, [ locationIDObject, plants[locationIDObject.index].location])
return (
<div className='plant-inps-wrapper'>
{
plants.map((item, idx) => {
return (
<div key={item.id} className="org-input-wrapper">
<input placeholder={`${item.isOffshore ? 'Offshore' : 'Plant ' + (idx+1) + ' location'}`} onChange={(e) => handlePlantLocChange(item.id, idx, e.target.value)} value={item.isOffshore ? 'Offshore' : item.location} className="org-input smaller-input"/>
</div>
)
})
}
</div>
)
I think your useCallback is not updating when value of your variables is changing and that is the issue:
Although the check is correct, but the call is made for older values of the variable. You should update the dependencies of your useCallback:
console.log(plants) inside getCoords might help you investigate.
Try this:
const handler = useCallback(debounce(getCoords, 5000), [plants]);
So it turns out the issue was with my debouncing function. I don't know what exactly the issue was, but everything worked as expected when I replaced the debouncing function with this:
useEffect(() => {
console.log("it changing")
const delayDebounceFn = setTimeout(() => {
getCoords(locationIDObject.id, locationIDObject.index)
}, 4000)
return () => clearTimeout(delayDebounceFn)
},[...plants.map(item => item.location), locationIDObject.id, locationIDObject.index])

React set state not updating object data

I'm trying to update state using handleChangeProps method, but some how finally the fields.fileName is set as empty string instead of actual value. I'm using material-ui DropZone for file and for name TextField. The addNsmFile is called when onSubmit is called. The remaining fields: name, fileData are not empty and the actual value is set and I can get them in addNsmFile function. Can you help me to figure out why fields.fileName is set as empty finally?
const [fields, setFields] = useState({
name : '',
fileName: '',
fileData: ''
})
const handleChangeProps = (name, value) => {
if (name === 'name' ) {
if (validator.isEmpty(value.trim())) {
setNameError('Enter NSM Name')
} else if (value.trim().length > 512) {
setNameError('The maximum length for NSM name is 512. Please re-enter.')
} else {
setNameError('')
}
}
if (name === 'fileData') {
console.log('fileData', value)
if (validator.isEmpty(value.trim())) {
setFileuploadError('Drag and drop or browse nsm file')
} else {
setFileuploadError('')
}
}
setFields({ ...fields, [name]: value })
}
const addNsmFile = () =>{
let nsmForm = new FormData()
nsmForm.append('name', fields.name)
nsmForm.append('fileName', fields.fileName)
nsmForm.append('fileData', fields.fileData)
NSMDataService.addFile(nsmForm).then((response)=>{
if (response.data.substring(0, 1) === '0') {
const results = JSON.parse(response.data.substring(2))
addNotification('success', 'NSM file is being added successfully.')
//props.onRowSelectionChange(results.addFile)
props.setOpenDialog(false)
props.setRefresh(results.addFile[0].id)
} else if (response.data.substring(0, 1) === '1') {
addNotification('error', response.data.substring(2))
props.setOpenDialog(false)
}
}).catch((error)=>{
console.log(error)
})
}
<DropzoneArea
acceptedFiles={[".csv, text/csv, application/vnd.ms-excel, application/csv, text/x-csv, application/x-csv, text/comma-separated-values, text/x-comma-separated-values"]}
maxFileSize={1000000000} //1GB
dropzoneText='Drag and drop a NSM file here or click to add'
showFileNames= {true}
showPreviews={false}
useChipsForPreview={true}
showAlerts={true}
filesLimit={1}
classes={{root:classes.rootDropzoneArea, icon: classes.iconDropzoneArea, text: classes.textDropzoneArea}}
onChange={(files) => {
files.forEach((file) => {
console.log(file)
handleChangeProps('fileName', file.name)
let reader = new FileReader()
reader.onload = function(event) {
let contents = event.target.result
handleChangeProps('fileData', contents)
console.log("File contents: " + contents)
}
reader.onerror = function(event) {
console.error("File could not be read! Code " + event.target.error.code)
}
reader.readAsText(file);
})
}}
onDelete={(file)=> {
handleChangeProps('fileData', '')
}
}
/>
I think the problem might be with your setFields() update in handleChangeProps. Try to do like this:
setFields(prevState => ({
...prevState,
[name]: value,
}))

Search Input for ANTD Table

I've managed to make a search input that allow search for the title and category of the project from the antd table but the initial data {dataSource} is not loaded with the data in dataLog (not sure is it because of AJAX request) and thus not loaded into the table, the data will only be shown when the first Search is performed at this case, here is my code:
const ListLogs = () => {
const [logs, setLogs] = useState([]);
const [search, setSearch] = useState("");
// const [latestFive, setLatestFive] = useState([]);
const [value, setValue] = useState("");
const timeAgo = (prevDate) => {
const diff = Number(new Date()) - prevDate;
const minute = 60 * 1000;
const hour = minute * 60;
const day = hour * 24;
const month = day * 30;
const year = day * 365;
switch (true) {
case diff < minute:
const seconds = Math.round(diff / 1000);
return `${seconds} ${seconds > 1 ? "seconds" : "second"} ago`;
case diff < hour:
return Math.round(diff / minute) + " minutes ago";
case diff < day:
return Math.round(diff / hour) + " hours ago";
case diff < month:
return Math.round(diff / day) + " days ago";
case diff < year:
return Math.round(diff / month) + " months ago";
case diff > year:
return Math.round(diff / year) + " years ago";
default:
return "";
}
};
const getAllLogs = async () => {
try {
const response = await fetch("http://localhost:5000/logs/");
const jsonData = await response.json();
setLogs(jsonData);
} catch (err) {
console.error(err.message);
}
};
const expandedRowRender = () => {
const columns = [
{ title: "Date", dataIndex: "date", key: "date" },
{ title: "Name", dataIndex: "name", key: "name" },
{
title: "Status",
key: "state",
render: () => (
<span>
<Badge status="success" />
Finished
</span>
),
},
{ title: "Upgrade Status", dataIndex: "upgradeNum", key: "upgradeNum" },
{
title: "Type",
dataIndex: "operation",
key: "operation",
render: () => {
return (
<div>
<Tag color="green">CREATE</Tag>
<Tag color="gold">UPDATE</Tag>
<Tag color="red">DELETE</Tag>
</div>
);
},
},
];
const data = [];
for (let i = 0; i < 3; ++i) {
data.push({
key: i,
date: "2014-12-24 23:12:00",
name: "This is production name",
upgradeNum: "Upgraded: 56",
});
}
return <Table columns={columns} dataSource={data} />;
};
const dataLog = [];
for (let i = 0; i < logs.length; i++) {
dataLog.push({
key: i,
category: <Tag color="default">{logs[i].category}</Tag>,
title: logs[i].title,
id: logs[i].id,
lastUpdated:
new Date(logs[i].last_updated).toLocaleString() +
" " +
timeAgo(new Date(logs[i].last_updated).getTime()),
});
}
const [dataSource, setDataSource] = useState(dataLog);
console.log("dataSource: ", dataSource);
console.log("dataLog: ", dataLog);
const columns = [
{
title: "Category",
dataIndex: "category",
key: "category",
},
{
title: "Title",
dataIndex: "title",
key: "title",
},
{ title: "ID", dataIndex: "id", key: "id" },
{ title: "Last Updated", dataIndex: "lastUpdated", key: "lastUpdated" },
];
useEffect(() => {
setDataSource(dataLog);
getAllLogs();
}, []);
return (
<Fragment>
<div>
<header className="headerPage">
<h1> Logs </h1>
</header>
</div>
<div className="container">
<Input.Search
placeholder="Input search text"
value={value}
onChange={(e) => {
const currValue = e.target.value;
setValue(currValue);
const filteredData = dataLog.filter(
(entry) =>
entry.title.toLowerCase().includes(currValue.toLowerCase()) ||
entry.category.props.children
.toLowerCase()
.includes(currValue.toLowerCase())
);
console.log("filtered Data: ", filteredData);
setDataSource(filteredData);
}}
// allowClear
// onChange={(e) => setSearch(e.target.value)}
// style={{ width: 200, float: "right" }}
/>
<Table
bordered
className="components-table-demo-nested"
onRow={(i) => ({
onClick: (e) => {
history.push(
`/admin/viewLog/${i.id}/${i.category.props.children}`
);
},
})}
columns={columns}
// expandable={{ expandedRowRender }}
dataSource={dataSource} //tried {dataSource ? dataSource: dataLog} does not work as well
size="small"
pagination={false}
/>
</div>
</Fragment>
);
};
export default ListLogs;
Please enlighten me for this! Thank you
Short answer:
When we call setDataSource(dataLog) inside the useEffect hooks for initial render (mounting) that would save empty array in dataSource variable, which can be used for showing empty list icon or loading, we have to set data source again when the data is fetched so, we can use another useEffect hook for logs state variable and set data source in it.
useEffect(() => {
setDataSource(dataLog);
}, [logs]);
Details:
What we are trying to achieve with these hooks i.e. useEffect, is that we can write a code which can react when defined action occur, e.g. what we did in useEffect is that on initial render (mount phase), we set empty array and then call the API. After that when data arrives we set logs i.e. setLogs(jsonData).
Which will populate the logs variable, then that loop come into play and dataLog will get filled but after that we never set the data source again with this DataLog i.e. filled list of objects (we only did that in for mount phase in which dataLog was empty, or when onChange get triggered)
So, a simple solution can be to use useEffect hooks for logs variable so, whenever the logs variable change, it will set the data source as well. As defined above in short answer.
Thus, with these hooks, we can significantly refactor this code.
One More Thing:
I recommend using getAllLogs function with await keyword, that will make that async code works like sync one i.e.
useEffect(async () => {
await getAllLogs();
}, []);
I (try to) reproduce it here

Inline MaterialTable Edit with DropDown

I'm attempting to create a MaterialTable with an inline editable field that has a dropdown. The problem seems to be in the columns object. With the lookup attribute, one can specify key:value pairs as dropdown list items. My dilemma seems to be that I am not able to iterate over a list and add the key-value pairs in the dynamic fashion below. It seems to only work when written like lookup:{ 1: "Test Value", 2: "Test Value 2" }. Please explain if my understanding is incorrect.
<MaterialTable
title="Available Attributes"
icons={this.tableIcons}
data={availableAttributesList}
columns={[
{ title: 'Attribute', field: 'name' },
{
title: 'Data Type',
field: 'dataType',
lookup: { dataTypePayload.map((attribute, name) => ({
attribute.id : attribute.dataType
}))}
}
]}
options={{
actionsColumnIndex: -1
}}
editable={{
onRowAdd: newData =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
const data = editableAvailableAttributesList;
data.push(newData);
this.setState({ data }, () => resolve());
}
resolve();
}, 1000);
}),
onRowUpdate: (newData, oldData) =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
const data = editableAvailableAttributesList;
const index = data.indexOf(oldData);
data[index] = newData;
this.setState({ data }, () => resolve());
}
resolve();
}, 1000);
}),
onRowDelete: oldData =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
let data = availableAttributesList;
const index = data.indexOf(oldData);
data.splice(index, 1);
this.setState({ data }, () => resolve());
}
resolve();
}, 1000);
})
}}
/>
The map creates an array of objects, which you have then placed inside another object.
As you've noticed, this won't work. To get the desired format, try this:
<MaterialTable
// ...other props
columns={[
{ title: 'Attribute', field: 'name' },
{
title: 'Data Type',
field: 'dataType',
lookup: dataTypePayload.reduce((acc: any, attribute: any) => {
acc[attribute.id] = attribute.dataType
return acc
}, {})
}
]}
// ...other props
/>
Hope that helps!

Resources