Error using custom hook inside a useEffect - javascript

let { photos, isQuering, empty, error } = useFetch(brand, isOld);
useEffect(() => {
if (isOld) {
const { photos: photosTest } = useFetch(brand, isOld);
photos = photosTest;
}
}, [isOld]);
useFetch is a custom hook that I have and I want to bring the old photos when the isOld state is true, the code above useEffect is called normally and the photos load, but I run into the error that useFetch is not being called inside the body a function component, the following error appears "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:", that is, I am doing something very wrong that I cannot to see! If you can help me, I would appreciate it very much!
Editing because of Danko! The Hook!
import { useEffect, useState, useContext } from 'react';
import { useScrollPagination } from './flow-manager';
import { db } from '../../Firebase';
import { userContext } from '../appContext';
export default function fetch(brand, isOld) {
const {
userData: { uid },
} = useContext(userContext);
const [photos, setPhotos] = useState([]);
const [lastDoc, setLastDoc] = useState(undefined);
const [isQuering, setIsQuering] = useState(false);
const [empty, setEmpty] = useState(false);
const [error, setError] = useState();
const [finished, setFinished] = useState(false);
const shouldFetchMore = useScrollPagination();
const [shouldKeepFecthing, setShouldKeepFetching] = useState(false);
useEffect(() => {
if (isQuering || finished) return;
if (!lastDoc || shouldFetchMore || shouldKeepFecthing) {
setIsQuering(true);
let query = !isOld
? db
.collection('catalog-images')
.where('brandName', '==', brand)
.orderBy('timestamp', 'desc')
.endBefore(new Date().setDate(new Date().getDate() - 40))
.limit(20)
: db
.collection('catalog-images')
.where('brandName', '==', brand)
.where('photoPeriod', '==', 'Antiga')
.limit(20);
if (lastDoc) query = query.startAfter(lastDoc);
query
.get()
.then(snap => {
const newPhotos = [];
let valid = 0;
snap.forEach(doc => {
const { url, pricetag, timestamp } = doc.data();
if (!uid && pricetag === 'Sim') return;
brand && newPhotos.push({ url, timestamp });
valid += 1;
});
setPhotos(oldPhotos => [...oldPhotos, ...newPhotos]);
setShouldKeepFetching(valid < 10);
setEmpty(snap.empty);
setLastDoc(snap.docs[snap.docs.length - 1]);
setFinished(snap.docs.length < 20);
setIsQuering(false);
})
.catch(setError);
}
}, [!!lastDoc, shouldFetchMore, shouldKeepFecthing, isQuering]);
return { photos, isQuering, empty, error, fetch };
}
Last Update:
Here, where I am calling the hook:
let {
photos,
isQuering,
empty,
error,
useFetch: refetch,
} = useFetch(brand, isOld);
useEffect(() => {
if (isOld) {
let { photos: photosTest } = refetch(brand, isOld);
photos = photosTest;
setIsOld(false);
}
}, [isOld]);
Aaaand, the hook:
import { useEffect, useState, useContext } from 'react';
import { useScrollPagination } from './flow-manager';
import { db } from '../../Firebase';
import { userContext } from '../appContext';
export default function useFetch(brand, isOld) {
const {
userData: { uid },
} = useContext(userContext);
const [photos, setPhotos] = useState([]);
const [lastDoc, setLastDoc] = useState(undefined);
const [isQuering, setIsQuering] = useState(false);
const [empty, setEmpty] = useState(false);
const [error, setError] = useState();
const [finished, setFinished] = useState(false);
const shouldFetchMore = useScrollPagination();
const [shouldKeepFecthing, setShouldKeepFetching] = useState(false);
useEffect(() => {
if (isQuering || finished) return;
if (!lastDoc || shouldFetchMore || shouldKeepFecthing) {
setIsQuering(true);
let query = !isOld
? db
.collection('catalog-images')
.where('brandName', '==', brand)
.orderBy('timestamp', 'desc')
.endBefore(new Date().setDate(new Date().getDate() - 40))
.limit(20)
: db
.collection('catalog-images')
.where('brandName', '==', brand)
.where('photoPeriod', '==', 'Antiga')
.limit(20);
if (lastDoc) query = query.startAfter(lastDoc);
query
.get()
.then(snap => {
const newPhotos = [];
let valid = 0;
snap.forEach(doc => {
const { url, pricetag, timestamp } = doc.data();
if (!uid && pricetag === 'Sim') return;
brand && newPhotos.push({ url, timestamp });
valid += 1;
});
setPhotos(oldPhotos => [...oldPhotos, ...newPhotos]);
setShouldKeepFetching(valid < 10);
setEmpty(snap.empty);
setLastDoc(snap.docs[snap.docs.length - 1]);
setFinished(snap.docs.length < 20);
setIsQuering(false);
})
.catch(setError);
}
}, [!!lastDoc, shouldFetchMore, shouldKeepFecthing, isQuering]);
return { photos, isQuering, empty, error, useFetch };
}

I'd suggest something else:
update your useFetch so it will have refetch function end add it to returned object.
now, your updated hook can be destructured like this: const { photos, isQuering, empty, error, refetch } = useFetch(brand);
your useEfect can be used like this:
useEffect(() => {
if(isOld) {
refetch();
setIsOld(false)
}
}, [isOld]);
Update:
You must rename your custon hook to start with use. Otherwise there is no way for react to differ it from other functions. So, instead of naming it fetch rename it to useFetch.

The thing is, you can't call a hook from another hooks. Hooks are only called from component body (top-level). Your code makes no sense on a few levels:
let { photos, isQuering, empty, error } = useFetch(brand, isOld);
useEffect(() => {
if (isOld) {
const { photos: photosTest } = useFetch(brand, isOld); // can't call a hook here
photos = photosTest; // can't mutate component-level variables
}
}, [isOld]);

Related

Getting Error: Rendered more hooks than during the previous render

When I load my Nextjs page, I get this error message: "Error: Rendered more hooks than during the previous render."
If I add that if (!router.isReady) return null after the useEffect code, the page does not have access to the solutionId on the initial load, causing an error for the useDocument hook, which requires the solutionId to fetch the document from the database.
Therefore, this thread does not address my issue.
Anyone, please help me with this issue!
My code:
const SolutionEditForm = () => {
const [formData, setFormData] = useState(INITIAL_STATE)
const router = useRouter()
const { solutionId } = router.query
if (!router.isReady) return null
const { document } = useDocument("solutions", solutionId)
const { updateDocument, response } = useFirestore("solutions")
useEffect(() => {
if (document) {
setFormData(document)
}
}, [document])
return (
<div>
// JSX code
</div>
)
}
useDocument hook:
export const useDocument = (c, id) => {
const [document, setDocument] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const ref = doc(db, c, id)
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false)
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id })
setError(null)
} else {
setError("No such document exists")
}
},
(err) => {
console.log(err.message)
setIsLoading(false)
setError("failed to get document")
}
)
return () => unsubscribe()
}, [c, id])
return { document, isLoading, error }
}
You cannot call a hook, useEffect, your custom useDocument, or any other after a condition. The condition in your case is this early return if (!router.isReady) returns null. As you can read on Rules of Hooks:
Donโ€™t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns...
Just remove that if (!router.isReady) returns null from SolutionEditForm and change useDocument as below.
export const useDocument = (c, id) => {
const [document, setDocument] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!id) return; // if there is no id, do nothing ๐Ÿ‘ˆ๐Ÿฝ
const ref = doc(db, c, id);
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false);
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id });
setError(null);
} else {
setError("No such document exists");
}
},
(err) => {
console.log(err.message);
setIsLoading(false);
setError("failed to get document");
}
);
return () => unsubscribe();
}, [c, id]);
return { document, isLoading, error };
};
The if (!router.isReady) return null statement caused the function to end early, and subsequent hooks are not executed.
You need to restructure your hooks such that none of them are conditional:
const [formData, setFormData] = useState(INITIAL_STATE)
const router = useRouter()
const { solutionId } = router.query
const { document } = useDocument("solutions", solutionId, router.isReady) // pass a flag to disable until ready
const { updateDocument, response } = useFirestore("solutions")
useEffect(() => {
if (document) {
setFormData(document)
}
}, [document])
// Move this to after the hooks.
if (!router.isReady) return null
and then to make useDocument avoid sending extra calls:
export const useDocument = (c, id, enabled) => {
and updated the effect with a check:
useEffect(() => {
if (!enabled) return;
const ref = doc(db, c, id)
const unsubscribe = onSnapshot(
ref,
(snapshot) => {
setIsLoading(false)
if (snapshot.data()) {
setDocument({ ...snapshot.data(), id: snapshot.id })
setError(null)
} else {
setError("No such document exists")
}
},
(err) => {
console.log(err.message)
setIsLoading(false)
setError("failed to get document")
}
)
return () => unsubscribe()
}, [c, id, enabled])
UseEffect cannot be called conditionally
UseEffect is called only on the client side.
If you make minimal representation, possible to try fix this error

LocalStorage does not save after page refresh (React context)

I am developing a context in which through a function I can send "pokemons" to a global array, and also send the information of this array to my localstorage so that it is saved in the browser, I managed to do that and the array items are in localstorage, but every time the site refreshes, localstorage goes back to the empty array.
import React, { useEffect, useState } from "react";
import CatchContext from "./Context";
const CatchProvider = ({ children }) => {
const [pokemons, setPokemons] = useState([], () => {
const dataStorage = localStorage.getItem('pokemons');
if (dataStorage) {
return JSON.parse(dataStorage)
} else {
return [];
}
});
useEffect(() => {
localStorage.setItem('pokemons', JSON.stringify(pokemons));
}, [pokemons]);
const updatePokemons = (name) => {
const updatedPokemons = [...pokemons];
const pokemonsIndex = pokemons.indexOf(name);
if (pokemonsIndex >= 0) {
updatedPokemons.slice(pokemonsIndex, 1)
} else {
updatedPokemons.push(name)
};
setPokemons(updatedPokemons)
}
const deletePokemon = async (name) => {
await pokemons.splice(pokemons.indexOf(toString(name)))
}
return (
<CatchContext.Provider value={{ pokemons: pokemons, updatePokemons: updatePokemons, deletePokemon: deletePokemon }}>
{children}
</CatchContext.Provider>
);
}
export default CatchProvider;
The problem is that useState doesn't take two arguments.
Instead of:
const [pokemons, setPokemons] = useState([], () => {
You want:
const [pokemons, setPokemons] = useState(() => {
I think you don't need to call useEffect on initial render so you can make use of refs for this
import { useEffect, useRef } from "react";
// other code....
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) {
localStorage.setItem('pokemons', JSON.stringify(pokemons));
} else {
didMount.current = true;
}
}, [pokemons]);

How do I access latest state in useEffect (when updating state) without including dependency

I'm trying to access the latest state in other setState function, cant figure out the correct way of doing it for a functional component
without accessing the latest state setMoviesListset state as undefined and causes issues
state
const [movies, setMoviesList] = useState();
const [currentGenre, setcurrentGenre] = useState();
const [page, setPage] = useState(1);
const [genreList, setGenreList] = useState();
const [nextPage, setNextPage] = useState(false);
const [previousMovieList, setPreviousMovieList] = useState();
useEffect(() => {
async function getMovies(currentGenre, page) {
if (currentGenre) {
const data = await rawAxios.get(
`https://api.themoviedb.org/3/discover/movie?api_key=f4872214e631fc876cb43e6e30b7e731&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}&with_genres=${currentGenre}`
);
setPreviousMovieList((previousMovieList) => {
if (!previousMovieList) return [data.data];
else {
if (nextPage) {
console.log(previousMovieList);
setNextPage(false);
return [...previousMovieList, data.data];
}
}
});
setMoviesList(previousMovieList.results);
} else {
const data = await rawAxios.get(
`https://api.themoviedb.org/3/discover/movie?api_key=f4872214e631fc876cb43e6e30b7e731&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}`
);
if (!previousMovieList) {
console.log('!previousMovieList', previousMovieList);
console.log('!data', data.data);
setPreviousMovieList(previousMovieList)
} else {
if (nextPage) {
console.log('else', previousMovieList);
setNextPage(false);
setPreviousMovieList([...previousMovieList, data.data])
// return [...previousMovieList, data.data];
}
}
setMoviesList(previousMovieList.results);
}
}
getMovies(currentGenre, page);
}, [currentGenre, page, setMoviesList, nextPage]);
want to access latest previousMovieList here
setMoviesList(previousMovieList.results);
You need to include previousMovieList in your useEffect dependency array as follows:
useEffect(()=>
{...},
[currentGenre, page, setMoviesList, nextPage, previousMovieList]
);
Without including it, you will have a stale closure and latest value will not be reflected in your function. This is causing the initial previousMovieList value of undefined to never update within your useEffect logic.
If you dont want it in your useEffect deps, you can use a ref:
const previousMovieList = useRef();
//then in your useEffect
setMoviesList(previousMovieList.current.results)
//and to set it
previousMovieList.current = ... // whatever you want to store
Or you can do something like this:
setPreviousMovieList((previousMovieList) => {
if (!previousMovieList) return [data.data];
else {
if (nextPage) {
console.log(previousMovieList);
setNextPage(false);
return [...previousMovieList, data.data];
}
}
setMoviesList(previousMovieList.results);
});
Basically move setMoviesList to within the setPreviousMovieList function where you do have access to previousMovieList.
Kind of hard to tell what you're trying to do, but generally when you want to store the previous value of state, you would use the ref approach. Like usePrevious for example
Add previousMovieList in useEffect dependency array, which allows react to know that its a dependency and reload when the dependency changes
you can use optional chaining to access the data coming from the api
enter code here
const [movies, setMoviesList] = useState();
const [currentGenre, setcurrentGenre] = useState();
const [page, setPage] = useState(1);
const [genreList, setGenreList] = useState();
const [nextPage, setNextPage] = useState(false);
const [previousMovieList, setPreviousMovieList] = useState();
useEffect(() => {
async function getMovies(currentGenre, page) {
if (currentGenre) {
const data = await rawAxios.get(
`https://api.themoviedb.org/3/discover/movie?api_key=f4872214e631fc876cb43e6e30b7e731&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}&with_genres=${currentGenre}`
);
setPreviousMovieList((previousMovieList) => {
if (!previousMovieList) return [data?.data];
else {
if (nextPage) {
console.log(previousMovieList);
setNextPage(false);
return [...previousMovieList, data?.data];
}
}
});
setMoviesList(previousMovieList?.results);
} else {
const data = await rawAxios.get(
`https://api.themoviedb.org/3/discover/movie?api_key=f4872214e631fc876cb43e6e30b7e731&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}`
);
if (!previousMovieList) {
console.log('!previousMovieList', previousMovieList);
console.log('!data', data?.data);
setPreviousMovieList(previousMovieList)
} else {
if (nextPage) {
console.log('else', previousMovieList);
setNextPage(false);
setPreviousMovieList([...previousMovieList, data?.data])
// return [...previousMovieList, data?.data];
}
}
setMoviesList(previousMovieList.results);
}
}
getMovies(currentGenre, page);
}, [currentGenre, page, setMoviesList, nextPage]);

Way to invoke function again while not setting different value in state

So I have built app which takes value from input -> set it to the state-> state change triggers functions in useEffect (this part is in custom hook) -> functions fetch data from api -> which triggers functions in useEffect in component to store data in array. The thing is that there are two problems that I am trying to solve :
When user is putting the same value in input and setting it in state it's not triggering useEffect functions (I solved it by wrapping value in object but I am looking for better solution).
When user uses the same value in short period of time api will send the same data which again makes problem with triggering function with useEffect (I tried to solved with refresh state that you will see in code below, but it looks awful)
The question is how can I actually do it properly? Or maybe the solutions I found aren't as bad as I think they are. Thanks for your help.
component
const [nextLink, setNextLink] = useState({ value: "" });
const isMounted = useRef(false);
const inputRef = useRef(null);
const { shortLink, loading, error, refresh } = useFetchLink(nextLink);
const handleClick = () => {
setNextLink({ value: inputRef.current.value });
};
useEffect(() => {
setLinkArr((prev) => [
...prev,
{
id: prev.length === 0 ? 1 : prev[prev.length - 1].id + 1,
long: nextLink.value,
short: shortLink,
},
]);
if (isMounted.current) {
scrollToLink();
} else {
isMounted.current = true;
}
inputRef.current.value = "";
}, [refresh]);
custom hook
const useFetchLink = (linkToShorten) => {
const [shortLink, setShortLink] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [refresh, setRefresh] = useState(false);
const isMounted = useRef(false);
const fetchLink = async (link) => {
setLoading(true);
try {
const response = await fetch(
`https://api.shrtco.de/v2/shorten?url=${link}`
);
if (response.ok) {
const data = await response.json();
setShortLink(data.result.short_link);
setRefresh((prev) => !prev);
} else {
throw response.status;
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isMounted.current) {
if (checkLink(linkToShorten.value)) {
setError(checkLink(linkToShorten.value));
} else {
fetchLink(linkToShorten.value);
}
} else {
isMounted.current = true;
}
}, [linkToShorten]);
const value = { shortLink, loading, error, refresh };
return value;
};
export default useFetchLink;

using usereducer with useeffect

I want to use useReducer instead of useState for data that is updated using useEffect in the codes below because this causes too much rerenders when they are used in a condition to update itself
const [complete, setComplete] = useState("");
const [userProfile, setUserProfile] = useState("");
const [displayName, setDisplayName] = useState("");
const [displayPicture, setDisplayPicture] = useState("");
useEffect(() => {
if (user.uid) {
const onChildAdd = database()
.ref("/User/" + user.uid)
.on("value", (snapshot) => {
setComplete(snapshot.val().Complete);
setUserProfile(snapshot.val().User);
setDisplayName(snapshot.val().displayName);
setDisplayPicture(snapshot.val().photoURL);
// ...
});
return () =>
database()
.ref("/User/" + user.uid)
.off("value", onChildAdd);
}
}, []);
below is a condition I am trying to use
function CheckInfo() {
if (!complete) {
setComplete("complete");
} else if (!displayName) {
setDisplayName("myName");
}
}

Categories