Getting Error: Rendered more hooks than during the previous render - javascript

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

Related

useFetch custom hook doesn't trigger inner useEffect

I'm trying to use a useFetch custom hook on a small todolist app that I'm working on to learn React.
I don't get why my useFetch function seems to work but its inner useEffect never triggers.
I tried removing the URL from dependencies array, adding the URL as an argument of the useEffect but nothing happened: my variable [response] stays null.
Here is the code for the useFetch :
utils.js:
export function useFetch(url) {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
console.log(url);
if (url === undefined) return;
const fetchData = async () => {
setIsLoading(true);
try {
const result = await getRequest(url);
setResponse(result);
setIsLoading(false);
} catch (error) {
setError(error);
}
};
fetchData();
}, [url]);
return [response, setResponse, error, isLoading];
}
App.js:
import { useState, useMemo, useCallback } from 'react';
import { useFetch, postRequest, deleteRequest, getFormatedDate } from './utils';
//more imports
export default function App() {
const [response] = useFetch('/items');
const [titleValue, setTitleValue] = useState('');
const [descriptionValue, setDescriptionValue] = useState('');
const [deadlineValue, setDeadlineValue] = useState(new Date());
const [doneFilter, setDoneFilter] = useState(0);
const [selectedItem, setSelectedItem] = useState();
const [showDialog, setShowDialog] = useState(false);
const onSave = useCallback(
async () => {
if (titleValue) {
let valueToSave = {};
valueToSave.title = titleValue;
valueToSave.status = false;
if (descriptionValue) valueToSave.description = descriptionValue;
valueToSave.deadline = deadlineValue instanceof Date ? deadlineValue : new Date();
setData((prev) => [...prev, valueToSave]);
setTitleValue('');
setDescriptionValue('');
setDeadlineValue(new Date());
try {
await postRequest('add', valueToSave);
} catch (err) {
console.error(err);
throw err;
}
}
},
[descriptionValue, titleValue, deadlineValue]
);
const onDelete = useCallback(async (item) => {
setData((items) => items.filter((i) => i !== item));
try {
await deleteRequest(item._id);
} catch (err) {
console.error(err);
throw err;
}
}, []);
const onModif = useCallback(async (id, field) => {
const res = await postRequest('update/' + id, field);
if (res.ok) setShowDialog(false);
}, []);
const organizedData = useMemo(() => {
if (!response) return;
for (let d of response) d.formatedDeadline = getFormatedDate(d.deadline);
response.sort((a, b) => new Date(a.deadline) - new Date(b.deadline));
if (doneFilter === 1) return response.filter((e) => e.status);
else if (doneFilter === 2) return response.filter((e) => !e.status);
else return response;
}, [response, doneFilter]);
//more code
return (
// jsx
)}
console.logging works just above the useEffect but never inside.
I cannot easily recreate your issue but I can point out some issues with your useFetch hook -
function useFetch(url) {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
console.log(url);
if (url === undefined) return;
const fetchData = async () => {
setIsLoading(true);
try {
const result = await getRequest(url);
setResponse(result);
setIsLoading(false);
} catch (error) {
setError(error);
// ❌ loading == true
}
};
fetchData();
// ❌ what about effect cleanup?
}, [url]);
return [response, setResponse, error, isLoading]; // ❌ don't expose setResponse
}
Check out Fetching Data from the react docs. Here's the fixes -
function useFetch(url) {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(
() => {
if (url == null) return;
let mounted = true // βœ… component is mounted
const fetchData = async () => {
try {
if (mounted) setIsLoading(true); // βœ… setState only if mounted
const response = await getRequest(url);
if (mounted) setResponse(response); // βœ… setState only if mounted
} catch (error) {
if (mounted) setError(error); // βœ… setState only if mounted
} finally {
if (mounted) setIsLoading(false); // βœ… setState only if mounted
}
};
fetchData();
return () => {
mounted = false // βœ… component unmounted
}
},
[url]
);
return { response, error, isLoading }
}
When you use it, you must check for isLoading first, then null-check the error. If neither, response is valid -
function MyComponent() {
const {response, error, isLoading} = useFetch("...")
if (isLoading) return <Loading />
if (error) return <Error error={error} />
return (
// response is valid here
)
}
See this Q&A for a more useful useAsync hook.

Why is my Firebase updateProfile function not working in my React App

I am trying to get my sign-up form to work using Firebase createUserWithEmailAndPassword and updateProfile functions in my React App. The sign-in function works (right panel), and I am able to see users in Firebase, however, when I try to create the displayName (left panel) I'm running into a reference issue.
My AuthContext provider has my signup function written like so:
export const useAuth = () => {
return useContext(AuthContext);
};
const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
const signup = (email, password) => {
return auth.createUserWithEmailAndPassword(email, password)
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = {
currentUser,
signup,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
};
I imported my useAuth function to my SignUp component and placed it inside my handleSubmit:
const { signup } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
if (passwordRef.current.value !== passwordConfRef.current.value) {
return setError("Please make sure passwords match.");
}
try {
setError("");
setLoading(true);
const user = await signup(
emailRef.current.value,
passwordRef.current.value
)
await updateProfile(user, {
displayName: usernameRef,
uid: user.user.uid,
})
// console.log(user);
navigate(`/dashboard/${user.user.uid}`);
} catch (e) {
setError("Something went wrong, please try again.");
console.log(e.message)
}
setLoading(false);
};
When the following console.log is run on the browser the following populates in the console:
find the error below
"Cannot access 'user' before initialization"
but I don't know what user reference it's referring to.
I read the following docs in reference to creating more login credentials for firebase:
https://firebase.google.com/docs/reference/js/v8/firebase.User#updateprofile
https://github.com/firebase/firebase-functions/issues/95
I originally had the updateProfie in a .then fucntion, however after some assistance I changed it await and now I'm getting a new error:
userInternal.getIdToken is not a function
The Promise chain isn't correct in the handleSubmit handler. The then-able should take a function instead of the result of immediately calling updateProfile. The user parameter likely hasn't been instantiated yet. In other words updateProfile is called when the component is rendered instead of when handleSubmit is called. You are also mixing the async/await and Promise-chain patterns. Use one or the other.
const handleSubmit = async (e) => {
e.preventDefault();
if (passwordRef.current.value !== passwordConfRef.current.value) {
setError("Please make sure passwords match.");
return;
}
try {
setError("");
setLoading(true);
await signup(
emailRef.current.value,
passwordRef.current.value,
);
navigate(`/dashboard/${user.user.id}`);
} catch (e) {
console.warn(e));
setError("Something went wrong, please try again.");
} finally {
setLoading(false);
}
};
You can call separately the updateProfile function when the currentUser state updates.
const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
const signup = (email, password) => {
return auth.createUserWithEmailAndPassword(email, password)
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
useEffect(() => {
if (currentUser) {
updateProfile({
/* profile properties you want to update */
});
}
}, [currentUser]);
const value = {
currentUser,
signup,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
};

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;

Have a javascript function pass a reference to itself in to another function

I found myself continuously writing the same shape of code for asynchronous calls so I tried to wrap it up in something that would abstract some of the details. What I was hoping was that in my onError callback I could pass a reference of the async function being executed so that some middleware could implement retry logic if it was necessary. Maybe this is a code smell that I'm tackling this the wrong way but I'm curious if it's possible or if there are other suggestions for handling this.
const runAsync = (asyncFunc) => {
let _onBegin = null;
let _onCompleted = null;
let _onError = null;
let self = this;
return {
onBegin(f) {
_onBegin = f;
return this;
},
onCompleted(f) {
_onCompleted = f;
return this;
},
onError(f) {
_onError = f;
return this;
},
async execute() {
if (_onBegin) {
_onBegin();
}
try {
let data = await asyncFunc();
if (_onCompleted) {
_onCompleted(data);
}
} catch (e) {
if (_onError) {
_onError(e ** /*i'd like to pass a function reference here as well*/ ** );
}
return Promise.resolve();
}
},
};
};
await runAsync(someAsyncCall())
.onBegin((d) => dispatch(something(d)))
.onCompleted((d) => dispatch(something(d)))
.onError((d, func) => dispatch(something(d, func)))
.execute()
I'm thinking you could use a custom hook. Something like -
import { useState, useEffect } from 'react'
const useAsync = (f) => {
const [state, setState] =
useState({ loading: true, result: null, error: null })
const runAsync = async () => {
try {
setState({ ...state, loading: false, result: await f })
}
catch (err) {
setState({ ...state, loading: false, error: err })
}
}
useEffect(_ => { runAsync() }, [])
return state
}
Now we can use it in a component -
const FriendList = ({ userId }) => {
const response =
useAsync(UserApi.fetchFriends(userId)) // <-- some promise-returning call
if (response.loading)
return <Loading />
else if (response.error)
return <Error ... />
else
return <ul>{response.result.map(Friend)}</ul>
}
The custom hook api is quite flexible. The above approach is naive, but we can think it through a bit more and make it more usable -
import { useState, useEffect } from 'react'
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [result, setResult] = useState(null)
const [error, setError] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
Custom hooks are dope. We can make custom hooks using other custom hooks -
const fetchJson = (url = "") =>
fetch(url).then(r => r.json()) // <-- stop repeating yourself
const useJson = (url = "") => // <-- another hook
useAsync(fetchJson, [url]) // <-- useAsync
const FriendList = ({ userId }) => {
const { loading, error, result } =
useJson("some.server/friends.json") // <-- dead simple
if (loading)
return <Loading .../>
if (error)
return <Error .../>
return <ul>{result.map(Friend)}</ul>
}

React Hook : Correct way of using custom hook to handle onClick Event?

As the title said, what is the correct way of using custom hook to handle onClick Event?
This codesandbox application will display a new quote on the screen when user clicks the search button.
function App() {
const [{ data, isLoading, isError }, doFetch] = useDataApi(
"https://api.quotable.io/random"
);
return (
<Fragment>
<button disabled={isLoading} onClick={doFetch}>
Search
</button>
{isError && <div>Something went wrong ...</div>}
{isLoading ? <div>Loading ...</div> : <div>{data.content}</div>}
</Fragment>
);
}
I created a custom hook called useDataApi() which would fetch a new quote from an API. In order to update the quote when the user clicks the button, inside the useDataApi(), I created a handleClick() which will change the value of a click value to trigger re-render. And this handleClick() function will be return back to App()
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [click, setClick] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const handleClick = () => {
setClick(!click);
};
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [initialUrl, click]);
return [{ data, isLoading, isError }, handleClick];
};
This is working, however, I don't feel this is the correct solution.
I also tried moving the fetchData() out of useEffect and return the fetchData(), and it works too. But according to the React Doc, it says it is recommended to move functions inside the useEffect.
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
useEffect(() => {
fetchData();
}, []);
return [{ data, isLoading, isError }, fetchData];
};
In addition, for creating these kinds of application, is the way that I am using is fine or there is another correct solution such as not using any useEffects or not create any custom Hook?
Thanks
Not sure if this is correct, but here is my solution.
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const doFetch = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
return [{ data, isLoading, isError }, doFetch];
};
Btw, don't mutate state directly.
const handleClick = () => {
setClick(!click); // don't do this
setClick(prev => !prev); // use this
};
Your implementation is fine. We are also using something similar. Hope you find it useful.
function useApi(promiseFunction, deps, shouldRun=true){
// promisFunction returns promise
const [loading, setLoading] = useState(false)
const [data, setData] = useState(false)
const [error, setError] = useState(false)
const dependencies: any[] = useMemo(()=>{
return [...dependencyArray, shouldRun]
},[...dependencyArray, shouldRun])
const reload = () => {
async function call() {
try {
setError(null)
setLoading(true)
const res = await promiseFunction();
}
catch (error) {
setError(error)
}
finally {
setLoading(false)
}
}
call();
}
useEffect(() => {
if(!shouldRun) return
setResult(null) //no stale data
reload()
}, dependencies)
return {loading, error, data, reload, setState: setData}
}
Below code will provide some idea about how to use it.
function getUsersList(){
return fetch('/users')
}
function getUserDetail(id){
return fetch(`/user/${id}`)
}
const {loading, error, data } = useApi(getUsersList, [], true)
const {loading: userLoading, error: userError, data: userData}
= useApi(()=>getUserDetail(id), [id], true)

Categories