React upload multiple files at once - javascript

I have the following code for uploading multiple images in my React app. The problem is that console.log(e) prints Progress Event object with all its values, but when I want to update my state I still be the default values, null, 0, []. I understand that onload is asynchronous and that might be the reason they are not updated. Technically the code is working when I upload file one by one. When I select multiple files at once, only the first one is being displayed. What am I doing wrong here?
const [fileUpload, setFileUpload] = useState(null);
const [filesUploaded, setFilesUploaded] = useState([]);
const [filesUploadedCount, setFilesUploadedCount] = useState(0);
const handleFileUpload = (e) => {
if (filesUploadedCount === 5 || e.currentTarget.files > 5) {
return;
}
const files = e.currentTarget.files;
console.log(files.length);
console.log(e.currentTarget.files);
Array.from(files).forEach((file: any) => {
const reader = new FileReader();
reader.onload = (e) => {
console.log(e); // Progress Event {}
setFileUpload(e.target.result);
setFilesUploadedCount(filesUploaded.length + 1);
setFilesUploaded([...filesUploaded, e.target.result]);
console.log(fileUpload); // null
console.log(filesUploaded); // []
console.log(filesUploaded.length); // 0
console.log(filesUploadedCount); // 0
};
reader.readAsDataURL(file);
});
};
Here I display them.
{filesUploaded?.map((file, index) => {
return (
<ItemImage
key={index}
src={file}
handleRemoveFile={handleRemoveFile}
/>
);
})}

useState is also asynchronous operation, so you should not rely on their values for calculating the next state. Pass a function like this. You may not see in your console.log because of that.
setFilesUploaded(prevState => [...prevState, e.target.result]);

Related

Should I use useCallback for a function that updates state?

I have a function handleFileSelect inside a React Component. It does some processing on the input file, and based on that it updates some states, which would trigger a rerender of the component.
But rerenders cause the function to be re-created. So will all the previous processed info be lost?
So should I use something like useCallback to prevent re-creation of my function?
And if that is the case, should'nt I do this for most functions?
const handleFileSelect = async(event: ChangeEvent < HTMLInputElement > ) => {
if (event.target.files && event.target.files ? .length > 0) {
setFormDisabled(true); // state-update
const file = event.target.files[0];
if (!imageWithinSizeLimit(file)) {
sendToast('error', 'File size beyond allowed range! Choose a file lesser than 0.5MB')
clearImageInput();
setFormDisabled(false); // state-update
return;
}
const valid = await validateImageType(file);
if (valid) {
const fileReader = new FileReader();
fileReader.readAsDataURL(file)
fileReader.onload = function(ev) {
// #ts-ignore
imagePreviewRef.current ? .setAttribute("src", ev.target.result)
}
setImageValid(true); // state-update
setFormDisabled(false) // state-update
}
else {
clearImageInput();
sendToast('error', "We only accept PNG or JPEG files as Avatar images")
setFormDisabled(false) // state-update
}
}
}
Simple answer: No.
If you don't need to track your function as dependency or you don't create component inside another component, then you don't need useCallback

React setTimeout with Loop

I am pulling documents from Firebase, running calculations on them and separating the results into an array. I have an event listener in place to update the array with new data as it is populated.
I am using setTimeout to loop through an array which works perfectly with the initial data load, but occasionally, when the array is updated with new information, the setTimeout glitches and either begins looping through from the beginning rather than continuing the loop, or creates a visual issue where the loop doubles.
Everything lives inside of a useEffect to ensure that data changes are only mapped when the listener finds new data. I am wondering if I need to find a way to get the setTimeout outside of this effect? Is there something I'm missing to avoid this issue?
const TeamDetails = (props) => {
const [teamState, setTeamState] = useState(props.pushData)
const [slide, setSlide] = useState(0)
useEffect(() => {
setTeamState(props.pushData)
}, [props.pushData])
useEffect(()=> {
const teams = teamState.filter(x => x.regData.onTeam !== "null" && x.regData.onTeam !== undefined)
const listTeams = [...new Set(teams.map((x) => x.regData.onTeam).sort())];
const allTeamData = () => {
let array = []
listTeams.forEach((doc) => {
//ALL CALCULATIONS HAPPEN HERE
}
array.push(state)
})
return array
}
function SetData() {
var data = allTeamData()[slide];
//THIS FUNCTION BREAKS DOWN THE ARRAY INTO INDIVIDUAL HTML ELEMENTS
}
SetData()
setTimeout(() => {
if (slide === (allTeamData().length - 1)) {
setSlide(0);
}
if (slide !== (allTeamData().length - 1)) {
setSlide(slide + 1);
}
SetData();
console.log(slide)
}, 8000)
}, [teamState, slide]);

Tracking changes in state?

The component code has several parameters, each of which has an initial value received from the server. How can I track that one of them (or several at once) has changed its state from the original one in order to suggest that the user save the changes or reset them?
Something similar can be seen in Discord when changing the profile / server.
The solution I found using useEffect () looks redundant, because there may be many times more options.
const [hiddenData, setHiddenData] = useState(server.hidden_data);
const [hiddenProfile, setHiddenProfile] = useState(server.hidden_profile);
const [isChanged, setIsChanged] = useState(false);
useEffect(() => {
if (hiddenData !== server.hidden_data
|| hiddenProfile !== server.hidden_profile) {
setIsChanged(true);
} else {
setIsChanged(false);
}
}, [hiddenData, server.hidden_data, hiddenProfile, server.hidden_profile]);
return (
<>
{isChanged && <div>You have unsaved changes!</div>}
</>
);
Maybe something like that?
const [draftState, setDraftState] = useState(server)
const [state, setState] = useState(server)
// a more complex object with the list of changed props is fine too
const isChanged = lodash.isEqual(state, draftState)
function changeProp (prop, value) {
setState({
...draftState,
[prop]: value
})
}
function saveState () {
setState(draftState)
// Persist state if needed
}

Add multiple files with onChange function and React Hooks, but singly

I need to add to files with react components. Here how I'm doing it with one file(onChange and onSubmit functions):
const onChangeFile = e => {
setFileData(e.target.files[0]);
};
const onSubmit = e => {
e.preventDefault();
const newItem = new FormData();
newItem.append('item', fileData);
const { description } = offerData;
newItem.append('description', description);
addItem(newItem);
setFileData(null);
}
Input(reactstrap):
<CustomInput
type="file"
name="item" id="item" label="Choose item image..."
onChange={onChangeFile}
/>
And here, how I'm doing it with multiple files, but with one input:
const onChangeFile = e => {
setFileData(e.target.files);
};
const onSubmit = e => {
e.preventDefault();
const newItem = new FormData();
for (const key of Object.keys(fileData)) {
newItem.append('item', fileData[key])
}
const { description1, description2 } = item;
newItem.append('description1', description1);
newItem.append('description2', description2);
addItem(newItem);
setFileData(null);
}
and input:
<CustomInput
type="file"
name="item"
id="item"
multiple
label="Add images/image..."
onChange={onChangeFile}
/>
And both works, but this time I want to add multiple files( two exactly), with two single inputs and my useState hook doesn't work(like that it isn't iterable). Here's how it looks like for both ways.
const [fileData, setFileData] = useState(null);
So, how to add one object with two images, but added with two single inputs?
Not sure if I fully understand. So the way you have it now you have a single input to receive data, however you want to be able to update your state from two inputs?
When you are setting state you are doing:
const onChangeFile = e => {
setFileData(e.target.files);
};
If this same handler is hooked up to another input, the second set of files will just override the first.
If you want to keep adding to your state you could either use an object, or an array. So onChange could look something like this:
const onChangeFile = e => {
// assuming here that e.target.files is an array already
const filesToAdd = e.target.files;
setFileData([...filesData, ...filesToAdd]);
};
Or with an object
const onChangeFile = e => {
const filesToAdd = e.target.files.reduce(
(map, file) => ({..., [file.name]: file}), {}));
const filesDataUpdate = {
...filesData,
...filesToAdd
}
setFileData(filesDataUpdate);
};
In this case I am assuming each file has a unique name. You could key it with any unique value.
Hope that helps!

Callback doesn't update state

The below function handle uploaded files, for some reason the setFiles doesn't update the files list after the callback ends so it causes the previous uploaded file to show up on the page, for example the user uploaded an image 1.jpg, nothing will show up on the page, next the user uploads a second file- now the first image 1.jpg will show up, and so on.
On setFiles the state is correct and updated but the return doesn't update the files state.
Any idea why?
const [files, setFiles] = useState([])
const addFiles = addedFiles => {
const newFiles = Array.from(addedFiles, file => newFileDecorator(file))
setFiles([...files, ...newFiles])
newFiles.forEach(file => {
file.reader.onload = async () => {
const dimensions = await getImageDimensions(file.reader.result)
setFiles(state => {
const index = state.findIndex(f => f.id === file.id)
state[index].readyState = file.reader.readyState
state[index].dimensions = dimensions
return state
})
}
file.reader.readAsDataURL(file.data)
})
}
You are mutating state without creating a new reference for it, so React skips the update as the shallow comparison indicates that they are the same object. Use this pattern instead.
setFiles(state => {
const file = state.find(f => f.id === file.id)
file.readyState = file.reader.readyState
file.dimensions = dimensions
return [ ...state, file ]
})

Categories