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

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

Related

Why does my react function function iterate twice?

I am currently trying out a project with the PokeAPI. And have used his guide for help. I can't get rid of the problem that the function iterates twice when called in the useEffect.
When I run the following code with the getAllPokemons in the useEffect
const PokeListPage = () => {
const [layoutToggle, setLayoutToggle] = useState(false);
const [allPokemons, setAllPokemons] = useState([]);
const [loadPoke, setLoadPoke] = useState(
"https://pokeapi.co/api/v2/pokemon?limit=20"
);
useEffect(() => {
getAllPokemons();
console.log(allPokemons);
}, []);
const getAllPokemons = async () => {
const res = await fetch(loadPoke);
const data = await res.json();
setLoadPoke(data.next);
function createPokemonObject(result) {
result.forEach(async (pokemon) => {
const res = await fetch(
`https://pokeapi.co/api/v2/pokemon/${pokemon.name}`
);
const data = await res.json();
setAllPokemons((currentList) => [...currentList, data]);
});
}
createPokemonObject(data.results);
console.log(allPokemons);
};
I get doublets of the first 20 objects in allPokemons. See output:
enter image description here
But when I remove the function and uses a button to trigger the function it behaves as expected. Which means that the function populates the allPokemon array with one object per pokemon. See output.
enter image description here
I have tried everything from copying entire files from other repositories and with an accuracy I didn't knew I had followed different tutorials but the problem remains. Does anyone know why?
It's bcz, you are rendering your app into a React. Strict mode component that runs specific functions and methods twice as a way to help you detect unintentional side effects. Since the side-effect is a state update, this triggers a rerender.
Use a useEffect to run the effect once when the component mounts.

How to update a Vue ref using a Watch inside a composable?

I have a Vue composable named getDocument that gets a Firebase document.
The code inside onSnapshot callback runs asynchronously. And I'm trying to update the document and error refs with values returned from onSnapshot.
But I want to avoid using a Watch outside the getDocument if possible, because always having to wrap my code in a Watch is a pain.
Instead I want to put a Watch inside the getDocument.ts and have it update the document and error refs there.
This is what I have so far, with no Watch inside getDocument.ts.
src/composable/getDocument.ts
import { ref, watchEffect, watch } from 'vue';
import { db } from 'src/firebase/config';
import {
doc,
onSnapshot,
DocumentSnapshot,
DocumentData,
} from 'firebase/firestore';
const getDocument = (collectionString: string, documentId: string) => {
const error = ref<string | undefined>();
const document = ref<DocumentData | undefined>();
const docRef = doc(db, collectionString, documentId);
const unsubscribe = onSnapshot(
docRef,
(doc: DocumentSnapshot<DocumentData>) => {
if (doc.data()) {
document.value = {
...doc.data(),
id: doc.id,
};
error.value = undefined;
} else {
error.value = "That document doesn't exist";
}
},
(err) => {
console.log(err.message);
error.value = 'Could not fetch documents';
}
);
// Cancel the listener when composable not in use
watchEffect((onInvalidate) => {
onInvalidate(() => {
unsubscribe();
});
});
// Maybe use a "Watch" here to update the doucment and error refs? But I can't get it working.
return { document, error };
};
export default getDocument;
Now when importing the getDocument composable I could wrap everything in a Watch to make sure the ref has a value. But I would rather do that inside getDocument instead.
For example:
src/composable/anotherComposable.ts
import getDocument from 'src/composables/getDocument';
const { document, error } = getDocument('users', 'USER_ID_HERE');
// I could wrap all my code here in a Watch, but I was hoping to avoid that. I want to use the Watch inside the getDocument composable to do the same thing.
watch(document, () => {
console.log(document.value);
});
// This is how I would like to ultimately use the document ref after the Watch is moved inside the getDocument composable. Currently this will show as undefined. So I need to somehow put a Watch inside the getDocument composable to make this have a value.
console.log(document.value);

How to re-render react component depends on a file?

I have a file that stores an array of objects. I have a component that fetches data from this file then render the list. The file could be updated somewhere else, I need the component to be updated if the file is modified. I have following code example
const header = () => {
const [list, setList] = useState([]);
// fetch
useEffect(() => {
const loadList = async () => {
const tempList = await getList("/getlist"); // get call to fetch data from file
setList(tempList);
};
loadList ();
}, [list]);
// function to render content
const renderList = () => {
return list.map(obj => (
<div key={obj.name}>
{obj.name}
</div>
));
};
return (
<div>{renderList()}</div>
)
}
// get call
router.get('/getlist',
asyncWrapper(async (req, res) => {
const result = await getList();
res.status(200).json(result).end();
})
);
const getList= async () => {
const list = JSON.parse(await fs.readFile(listPath));
return list;
}
Code has been simplified. If I remove the list from useEffect, then it will only render once and will never update unless I refresh the page. If I include list there, loadList() will get called constantly, and component will get re-rendered again and again. This is not the behavior I want. I am just wondering without making header component async component, how do I only re-render this component when the file is changed?
Thank you very much.
There are two approaches you can take to this:
Polling
Request the URL on an interval, and clear it when the component is unmounted.
Replace loadList () with:
const interval = setInterval(loadList, 60000); // Adjust interval as desired
return () => clearInterval(interval)
Make sure the cache control headers set in the response to /getlist don't stop the browser from noticing updates.
Server push
Rip out your current code to get the data and replace it with something using websockets, possibly via Socket.IO. (There are plenty of tutorials for using Socket.io with React that can be found with Google, but its rather too involved to be part of a SO answer).

Why is my custom hook called so many times?

I'm trying to implement a custom hook to provide the app with a guest shopping cart. My hook wraps around the useMutation hook from Apollo and it saves the shopping cart id in a cookie while also providing a function to "reset" the cart (basically, to remove the cookie when the order is placed).
Code time! (some code omitted for brevity):
export const useGuestCart = () => {
let cartId;
const [createCart, { data, error, loading }] = useMutation(MUTATION_CREATE_CART);
console.log(`Hook!`);
if (!cartId || cartId.length === 0) {
createCart();
}
if (loading) {
console.log(`Still loading`);
}
if (data) {
console.log(`Got cart id ${data.createEmptyCart}`);
cartId = data.createEmptyCart;
}
const resetGuestCart = useCallback(() => {
// function body here
});
return [cartId, resetGuestCart];
};
In my component I just get the id of the cart using let [cartId, resetCart] = useGuestCart(); .
When I run my unit test (using the Apollo to provide a mock mutation) I see the hooked invoked several times, with an output that looks something like this:
console.log src/utils/hooks.js:53
Hook!
console.log src/utils/hooks.js:53
Hook!
console.log src/utils/hooks.js:59
Still loading
console.log src/utils/hooks.js:53
Hook!
console.log src/utils/hooks.js:62
Got cart id guest123
console.log src/utils/hooks.js:53
Hook!
console.log src/utils/hooks.js:53
Hook!
I'm only getting started with hooks, so I'm still having trouble grasping the way they work. Why so many invocations of the hook?
Thank you for your replies!
Think of hooks as having that same code directly in the component. This means that every time the component renders the hook will run.
For example you define:
let cartId;
// ...
if (!cartId || cartId.length === 0) {
createCart();
}
The content inside the statement will run on every render as cartId is created every time and it doesn't have any value assigned at that point. Instead of using if statements use useEffect:
export const useGuestCart = () => {
const [cartId, setCartId] = useState(0);
const [createCart, { data, error, loading }] = useMutation(
MUTATION_CREATE_CART
);
const resetGuestCart = () => {
// function body here
};
useEffect(() => {
if(!cartId || cartId.length === 0){
createCart();
}
}, [cartId]);
useEffect(() => {
// Here we need to consider the first render.
if (loading) {
console.log(`Started loading`);
} else {
console.log(`Finished loading`);
}
}, [loading]);
useEffect(() => {
// Here we need to consider the first render.
console.log(`Got cart id ${data.createEmptyCart}`);
setCartId(data.createEmptyCart);
}, [data]);
return [cartId, resetGuestCart];
};
Also notice that there is no actual benefit from using useCallback if the component which is receiving the function is not memoized.

Store data from useQuery with useState

I'm using React hooks both to fetch GraphQL data with react-apollo and to store local state:
const [userData, setUserData] = useState({})
const { loading, error, data } = useQuery(USER_QUERY)
However, I'm wondering how to store data to userData. Is this how it's supposed to work:
useEffect(() => {
setUserData(data)
}, [Object.entries(data).length])
Looks like what you have probably works. There is also a onCompleted option available in the options parameter. it takes a callback of type:
(data: TData | {}) => void
so this is another way of doing it:
const { loading, error, data } = useQuery(USER_QUERY, {onCompleted: setUserData})
What are you trying to do with the returned data that you are unable to accomplish by simply using it as destructured from the query hook? In most use cases it can be used immediately, as it will update itself when refetched.
If it is necessary (and it could be), as the other answer says, the useEffect hook you posted should work, but I would replace the dependency with simply data, to prevent an edge case where the response has an equal length consisting of different data and does not update:
useEffect(() => {
setUserData(data)
}, [data])
I think something like this would work - you will need to create the initial state with useState, could be empty array and then onComplete in the useQuery would setTranscationsData... it is triggered every render when state or props change. Could of course add an inital state inside useState which insn't an empty array.
const [transactionsData, setTransactionsData] = React.useState([]);
const { error, data } = useQuery(GET_TRANSACTIONS, {
onCompleted: () => {
setTransactionsData(data.transactions);
},
});
another example
const [getLegalStatement] = useLazyQuery(GET_LEGAL_STATEMENT, {
fetchPolicy: 'network-only',
onCompleted: (data) => {
setTempLegalStatement(data.getLegalStatement);
},
onError: () => {
setTempLegalStatement({
consentedLegalStatementHash: '',
consentedSuppliersHash: '',
statement: '',
suppliersModal: '',
});
setTimeout(() => {
setRefetchNeeded(true);
}, 10000);
},
});
Use onSuccess
const [userData, setUserData] = useState({})
const { data, isLoading, error } = useQuery('QueryKey', QueryFunction, { onSuccess: setUserData })
This onSuccess callback function will fire setUserData(data) for you automatically any time the query successfully fetches new data.
To elaborate above, you can't use onSuccess/onSettled because those will not rerun if the data is cached, so if you leave the component and come back before the query expires your data won't get set.

Categories

Resources