File attachment is taking too much time when using React Drop zone - javascript

I am using the react-dropzone library to drag or select files from the browser.
I have the below component to let the user select multiple files. Everything works except that if the user chooses more files let say 2 or more files with size 1 MB, the selection of documents is taking time. If the number of files is more, the more time it takes to selected the user-selected files.
As per my reading of the react-dropzone docs, it processes files as soon as it uploads. So I tried setting autoProcessQueue='false'.
but no luck.
I just want to let the user select all the files without blocking for 30 or 40 secs after selecting files, and before send to the backend server.
I did the debug and the setSelectedUserFiles() is reached after all the files are processed internally by the DropZone. I am not sure if there is a way to disable it and let it process as part final submission of the form or click on the button.
In case if we can't achieve the same, is there a way to show the message to the user that files are being attached.
Any help will be appreciated.
Below is my react component
const SelectUserFiles = () => {
const [userName,setUserName] = userState('TestUser')
const [selectedUserFiles,setSelectedUserFiles] = userState([])
const handleUserFileUpload = async (acceptedFiles) => {
await setSelectedUserFiles(acceptedFiles)
}
return (
<div className='myClass'>Select Files</div>
<Dropzone
//autoProcessQueue='false'
accept={'.pdf'}
onDrop={acceptedFiles => handleUserFileUpload(acceptedFiles)}
>…</Dropzone>
</div>
<MyButton>
//logic to send the files to backend axios with the files selectedUserFiles
</MyButton>
)
}

Try something like this
Create separate api component that only send files,
Your component that has dropzone manages file upload
locally without sending to the server. You only click the button
and send it.
this link was very helpful:
https://www.robinwieruch.de/react-hooks-fetch-data
first component:
export const useApiCall = () => {
const [data, setData] = useState({});
const [selectedAllUserFiles, setSelectedAllUserFiles] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const pushToserver = async () => {
setIsError(false);
setIsLoading(true);
try {
const formData = new FormData();
Array.from(selectedAllUserFiles).forEach((file) => {
form.append('fileData', file);
});
const result = await axios.post(someUrl, formData);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
if (selectedAllUserFiles.length > 0) {
pushToserver();
}
}, [selectedAllUserFiles]);
//passing reference to the calling component
return [{ data, isLoading, isError }, setSelectedAllUserFiles];
};
Second component:
const SelectUserFiles = () => {
const [userName,setUserName] = userState('TestUser')
const [selectedUserFiles,setSelectedUserFiles] = userState([])
//Ref from logical api call component
const [{ data, isLoading, isError }, setSelectedAllUserFiles] = useApiCall();//new component
const handleUserFileUpload = async (acceptedFiles) => {
if (acceptedFiles) {
acceptedFiles.map((file) => {
setSelectedUserFiles((selectedUserFiles) => selectedUserFiles.concat(file));
return selectedUserFiles;
});
}
//await setSelectedUserFiles(acceptedFiles)
}
return (
<div className='myClass'>Select Files</div>
<Dropzone
//autoProcessQueue='false'
accept={'.pdf'}
onDrop={acceptedFiles => handleUserFileUpload(acceptedFiles)}
>…</Dropzone>
</div>
//on click call api and pass collected user files all together
<MyButton onClick={()=>setSelectedAllUserFiles(selectedUserFiles)}>
//logic to send the files to backend axios with the files selectedUserFiles
</MyButton>
)
}

Related

How to handle refresh for data filtering in React useEffect?

I have the following code in my React component:
const { id } = useParams();
const { tripData, facilityData } = useContext(AppContext);
const [data, setData] = useState([]);
useEffect(() => {
const idResults = facilityData.filter(facility => facility.id === id);
if (idResults.length > 0) {
setData(idResults[0]);
}
}, [])
Where:
[data, SetData] is the state that is used to handle populating a container
facilityData is data accessed from my app context
id is accessed from the URL
What seems to happen is that the data loads the first time without fault, but it errors out when hosted on the actual site (on localhost, it waits and eventually loads). To try to get a better idea of what was happening, I tried the following code:
const { id } = useParams();
const { tripData, facilityData } = useContext(AppContext);
const [data, setData] = useState([]);
useEffect(() => {
const idResults = facilityData.filter(facility => facility.id === id);
if (idResults.length > 0) {
setData(idResults[0]);
} else if (idResults.length === 0) {
console.log(`id: ${id}`)
console.log(`len: ${idResults}`)
}, [])
On localhost, on refresh, it console logs the actual id but then console logs the empty array before finally loading the data.
What I'm wondering is why this is the observed behavior. The "id" value seems to be constantly available, but the filter doesn't seem to run prior to the site loading. Is there a way to prevent this?
EDIT:
This is how I get the data (from Firebase)
App.js
import { collection, getDocs } from "firebase/firestore";
import { db } from "./firebase";
const [truckData, setTruckData] = useState([]);
const [facilityData, setFacilityData] = useState([]);
const [tripData, setTripData] = useState([]);
useEffect(() => {
const fetchData = async (resource, setter) => {
let list = [];
try {
const querySnapshot = await getDocs(collection(db, resource));
querySnapshot.forEach((doc) => {
let docData = doc.data();
if (resource === "trips") {
docData.startDate = docData.startDate.toDate();
docData.endDate = docData.endDate.toDate();
}
list.push({ id: doc.id, ...docData });
});
setter(list);
} catch (error) {
console.log(error);
}
};
fetchData("trucks", setTruckData);
fetchData("facilities", setFacilityData);
fetchData("trips", setTripData);
}, []);
The app is at logi-dashboard, if that helps any.
EDIT Turns out the issue was with my hosting service, not the project. Go figure.
Based on my understanding, it seems like the facilityData on which you are trying to apply filter and which is coming from AppContext(Context hook variable) is found to be empty array when the useEffect code is getting executed, this might be scene if you are hitting any API to get the data into facility but the API response is not coming till the time useEffect is getting executed or any other source which is not populating the facilityData until useEffect runs.
In that case, you can add facilityData in the dependency array of useEffect, which will help the useEffect execute again once the facilityData is populated(updated)

Firebase fetching + React: Images won't load and getting a 500 error, but if I keep refreshing they show up

So I'm currently brushing up my react skills and learning firebase. I found it simple enough to configure and now I have been successfully making fetch requests from my realtime database.
I am currently building some sort of a birthday app that would show monthly and daily celebrants, and have generated mock data that I imported into my database. Currently, the first names are being fetched successfully and displaying on the screen, however the images (that are hosted on another site and fetched as a string from the db) are not loading properly and I'm getting 500 error in my console. If I keep refreshing though, they eventually load.
I'm thinking it must be with the way I make my fetch request. I basically fetch all users and then make a filter (have not explored fetching with queries yet) so I thought it would work.
This is the code for the fetch requests.
export const getUsers = () => {
const usersDb = ref(database);
return get(child(usersDb, `/users`)).then((snapshot) => {
return snapshot.val();
});
};
export const getMonthlyCelebrants = async () => {
const users = await getUsers();
const monthlyCelebs = [];
for (let user in users) {
const userMonth = +users[user]["birth_date"].split("/")[1];
userMonth === getMonthNum() && monthlyCelebs.push(users[user]);
}
return monthlyCelebs;
};
And this is the Monthly Celebrants component I use them in:
export default function Monthly() {
const [monthlyCelebs, setMonthlyCelebs] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function loadPage() {
setIsLoading(true);
const users = await getMonthlyCelebrants();
setIsLoading(false);
setMonthlyCelebs(users);
}
loadPage();
}, []);
return (
<div>
<h2>{getMonthAndYear()} Celebrants</h2>
{isLoading ? (
<p>Celebrants loading...</p>
) : (
sortBirthdays(monthlyCelebs).map((monthlyCeleb) => {
return (
<SingleMonthlyCelebrant
monthlyCeleb={monthlyCeleb}
key={`${monthlyCeleb.birth_date}${monthlyCeleb.first_name}`}
/>
);
})
)}
</div>
);
}
Any tips or advice would be greatly appreciated. Thank you!

React Dropzone - async function in onDrop callback doesn't update state

I'm trying to use react-dropzone onDrop with useCallback function to update the array of files dropped onto the area and trigger the upload. My code looks like this:
const [files, setFiles] = useState([]);
...
const onDrop = useCallback((acceptedFiles) => {
setFiles(acceptedFiles);
handleFileUpload();
}, []);
const {
getRootProps,
getInputProps,
isDragActive,
} = useDropzone({ onDrop });
handleFileUpload is an asynchronous function, which uses Fetch API. When I try to log the files array inside this method, it appears to be empty, even though I updated the state before running this function.
const handleFileUpload = async () => {
console.log(files); // <- returns empty array
}
I also tried to set different variables to indicate if the upload has started, etc. but changing the values within this method doesn't update the state of my component at all. Is it even possible to use an async function in a callback like this? Should I trigger the file upload somewhere else? I feel like I don't quite understand the concept here.
Your state will not update until after your code exits and/or relinquishes control. i.e. after you call setFiles() the files variable will still be STALE until the code returns to the event loop.
Just pass the accepted files into the function.
const onDrop = useCallback((acceptedFiles) => {
setFiles(acceptedFiles); // <-- that won't update right away
handleFileUpload(acceptedFiles);
}, []);
UPDATE
Here are snippets from one of my projects. I just stuff the dropped file into state, as you do. That causes the component to re-render, and when it does, I catch the updated file state in a hook, async parse the file, and return the data - which is then consumed by the component. The component shows UI states - importing if fileToImport is set and dataToImport is not yet available, etc...
// MyComponent.tsx
const MyComponent = () => {
const [fileToImport, setFileToImport] = useState<File | undefined>()
const [dataToImport, dataToImportError] = useReadFileData(fileToImport)
const onDrop = useCallback(acceptedFiles => {
setFileToImport(acceptedFiles[0])
}, [])
//useREadFileData.ts hook
import { useEffect, useState } from 'react'
const useReadFileData = (file: File | undefined): [any[], string | undefined] => {
const [rows, setRows] = useState<unknown[]>([])
const [error, setError] = useState<string>()
useEffect(() => {
async function parseFile(file: File) {
try {
// snip
setRows(rows)
} catch (error: any) {
if (typeof error === "object") {
setError(error?.message ?? "Error parsing file")
}
}
}
// only if we have a file to parse
if (file) {
parseFile(file)
}
}, [file, setError, setRows])
return [rows, error]
}
export default useReadFileData

How to share a dynamically generated image from react with react-share?

I have a react app which generates images on the front end dynamically using Plotly.js. I'd like to add image sharing functionality. I am trying to use react-share for this. Social platforms require image URL for image sharing and do not support images in base64 encoding or alike. Backend was implemented so it can receive images in base64, store in the database and return URL to the image, which is then used for sharing with react-share.
As the image is generated dynamically (it changes each time user resizes the chart, for instance), everything should be done when user clicks on Share icon.
So after the user has clicked on the Share icon, the image generated on the front end should be saved to back end
let imgURI;
const handleClick = () => {
Plotly.toImage('chartContainer', {
format: 'png',
width: 1000,
height: 600
})
.then(dataUrl => api.post('/image/base64ToPng', { image: dataUrl })
.then(
(response) => {
imgURI = response.data.imgURI;
},
failure => console.error(failure)
));
};
after the response is received, passed down to the sharing component like this
<FacebookShareButton
url={imgURI}
>
<FacebookIcon/>
</FacebookShareButton>
The code sample is not asynchronous, so the image URI is not passed to the sharing component, therefore sharing does not work. I tried to pass the prop down using conditional depending on whether it's defined or not and did not come up with a solution. I also looked up some issues in react-share repo that dealt with async urls, but seems like none of them deals with the dynamic image sharing on click.
I'd very appreciate a hint on how to complete this task.
This is serious hack territory, and the whole thing would be a lot simpler if this PR had been completed.
However, the code below should work (see codesandbox).
The key steps are:
Have a bit of state that keeps track of whether you have a url from the service or not.
When this state is "none", disable the facebook button's default behavior (i.e. openShareDialogOnClick = false)
Add an onClick handler to the facebook button that asynchronously fetches the url and sets the state (triggering a re-render)
Use an effect + ref so that when the url is set to something real, you manually call the click event on the button (which now has a real address in its url prop), and then re-sets the url to "none"
import { useEffect, useRef, useState } from "react";
import { FacebookIcon, FacebookShareButton } from "react-share";
async function getUrFromService(): Promise<string> {
// The real implementation would make a network call here.
await new Promise((resolve) => setTimeout(resolve, 1000));
return "https://via.placeholder.com/150";
}
export default function App() {
const shareButton = useRef<HTMLButtonElement>(null);
const [url, setUrl] = useState<string>("none"); // Unfortunately, we have to have a dummy string here, or FacebookShareButton will blow up.
// Provide an onClick handler that asyncronously fetches the url and sets it in the state.
const onClick = async () => {
// Be sure to check for the "none" state, so we don't trigger an infinite loop.
if (url === "none") {
const newUrl = await getUrFromService();
setUrl(newUrl);
}
};
// Whenever "url" changes and we re-render, we manually fire the click event on the button, and then re-set the url.
useEffect(() => {
if (url !== "none") {
shareButton.current?.click();
setUrl("none");
}
}, [url, shareButton]);
return (
<FacebookShareButton
ref={shareButton}
// Disable calling the dialog if we don't have a url yet.
openShareDialogOnClick={url !== "none"}
url={url}
onClick={onClick}
>
<FacebookIcon />
</FacebookShareButton>
);
}
Use navigator.share api if possible.
Having, said that you could write a wrapper that manages loading state and disables icon of component.
Create a async share component
//AsyncShareLoader.jsx
const AsyncShareLoader = ({ url, children }) => {
const loading = !url;
return (
<div style={{ filter: `grayscale(${loading ? "100%" : "0%"}` }}>
{React.Children.map(children, (child) =>
React.cloneElement(child, {
disabled: loading,
url: loading ? "none" : url,
openShareDialogOnClick: !loading
})
)}
</div>
);
};
Now, use this as a wrapper for your actual react-share icons. Something like this
const Share = () => {
const [url, setUrl] = useState()
useEffect(() => {
fetch('/imgUrl').then(getUrlFromRes).then(setUrl)
}, [])
return (
<AsyncShareLoader url={url}>
<FacebookShareButton>
<FacebookIcon />
</FacebookShareButton>
</AsyncShareLoader>
);
}
Extending this techinique you could manually steal click as Andrew suggested
const getBase64ImageFromUrl = url =>
fetch(url)
.then(response => response.blob())
.then(
blob =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
blob = new Blob([blob], {type: 'image/png'});
reader.readAsDataURL(blob);
}),
);
const shareData = async () => {
await getBase64ImageFromUrl('https://url of image').then(base64 => {
const shareOption = {
title: App Link,
url: base64,
message:
'Download app now: ,AppLink:""',
};
try {
const shareResponse = Share.open(shareOption);
console.log('share response ,', shareResponse);
} catch (error) {
alert(error.message);
}
});
};

Reset React state before a render

I'm building a web application that consumes TMDB Api. I have the following code that fetch all information about a TV Show
export const useShowInfoFetch = ({showId}) => {
const [data, setData] = useState({})
const [loading, setLoading] = useState(false)
const [_error, _setError] = useState(false)
const fetchShowInfo = useCallback(() => {
setLoading(true)
try {
axios.get(getShowInfo(showId))
.then(response => {
setData(response.data)
})
} catch (error) {
_setError(true)
} finally {
setLoading(false)
}
}, [showId])
useEffect(() => {
fetchShowInfo()
}, [fetchShowInfo])
return [data, loading, _error]
}
All the information fetched is displayed in page, that also has Links with react-router-dom. Those links goes to another tv show page.
The problem is that when I'm in a page with a tv show that has X amount of seasons and I click a tv show with less seasons, the seasons from the page I was are persisting for a little bit of time. So, when I fetch the information for each season I got a 404 in the page that has less seasons.
Here is a screenshot of the error
The orange circle is what it's displayed since I click the tv show with less seasons.
As you can see, the seasons from the previous page are persisting for a little time, and because The Alienist has only 2 seasons (not 9) I get the 404. You can also note that latter, the correct amount of seasons are displayed.
I've tried to add a cleanup method in the useEffect hook. Something like this:
useEffect(() => {
fetchShowInfo()
return function cleanup() {
setData({})
}
}, [fetchShowInfo])
But this did not work.
I know that I can handle that with a catch after the then Axios promise, but I want to figure out why this is happening and fix the issue with a good solution instead of avoiding it.
Any help is welcomed and I can share the repository with all the code if needed.
EDIT:
To display the similar movies I use another custom hook
export const useSimilarFetch = (elementType, elementId) => {
const [similarElements, setSimilarElements] = useState({elements: []})
const [similarLoading, setSimilarLoading] = useState(false)
const [_error, _setError] = useState(false)
const fetchSimilarElements = useCallback(async (endpoint) => {
console.log(">>> fetching similar elements <<<")
setSimilarLoading(true)
try {
await axios.get(endpoint)
.then(response => {
setSimilarElements(() => ({
elements: [...response.data.results],
currentPage: response.data.page,
totalPages: response.data.total_pages
}))
})
} catch (error) {
_setError(true)
} finally {
setSimilarLoading(false)
}
}, [])
useEffect(() => {
fetchSimilarElements(getSimilar(elementType, elementId));
}, [fetchSimilarElements, elementType, elementId])
return [{similarElements, similarLoading, _error}, fetchSimilarElements]
}
Then, in my ShowInfoComponent I call all the needed hooks like this:
const {showId} = useParams()
const [data, loading, _error] = useShowInfoFetch({showId})
const [{similarElements, similarLoading}] = useSimilarFetch("tv", showId)
Thanks.
By the time showId changes, data has to wait one additional render cycle, so showId is already used even though data has not yet been fetched. The UI relies on both showId and data, yet data depends on showId. One way to solve this could be having your UI to rely on data alone. What about the id? Add it to data for example. We merely want to avoid the desynchronization.
Something like this:
export const useShowInfoFetch = ({showId}) => {
const [data, setData] = useState({})
const [loading, setLoading] = useState(false)
const [_error, _setError] = useState(false)
const fetchShowInfo = useCallback(() => {
setLoading(true)
try {
axios.get(getShowInfo(showId))
.then(response => {
setData({ id: showId, info: response.data})
})
} catch (error) {
_setError(true)
} finally {
setLoading(false)
}
}, [showId])
useEffect(() => {
fetchShowInfo()
}, [fetchShowInfo])
return [data, loading, _error]
}
Then use data.id to build your links.
If response.data already contains the id, then even better, use that.
That's just an example, of course but hopefully you get the idea.
I might be wrong but I believe you are not watching the correct value on the useEffect. You should be watching showId and not the function fetchShowInfo. That is:
useEffect(() => {
fetchShowInfo()
}, [showId]) --> HERE
And as you are memoized the callback, if you are watching the wrong variable then you will get back the 'last answered'.

Categories

Resources