useFetch custom hook doesn't trigger inner useEffect - javascript

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.

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

React: Update one state variable using another state variable

What I am trying to achieve:
Retrieve book-> take book.chapterIds[0] to update currentChapter -> take currentChapter to update chapters
I am using one state variable(Book) to set another state variable(chapters), like so:
useEffect(() => {
getBook(match.params.id);
// eslint-disable-next-line
}, []);
useEffect(() => {
setCurrentChapter(book.chapterIds[0]);
// eslint-disable-next-line
}, [book]);
useEffect(() => {
getChapter(currentChapter);
// eslint-disable-next-line
}, [currentChapter]);
For second useEffect, I end up getting: Uncaught TypeError: book.chapterIds is undefined
Here is what I tried:
useEffect(() => {
if (Object.keys(book).length !== 0) {
setCurrentChapter(book.chapterIds[0]);
}
// eslint-disable-next-line
}, [book]);
which kinda works, but I still ends up triggering:
useEffect(() => {
getChapter(currentChapter);
// eslint-disable-next-line
}, [currentChapter]);
where both book and currentChapter is undefined
App.js
const [book, setBook] = useState({});
const [chapters, setChapters] = useState({});
const [currentChapter, setCurrentChapter] = useState();
const [loading, setLoading] = useState(false);
const getBook = async (id) => {
setLoading(true);
const res = await axios.get(`<someurl><with id>`);
console.log(res.data);
setBook(res.data.book);
setLoading(false);
};
const getChapter = async (chapterId) => {
if (chapters[chapterId] === undefined) {
console.log(`<someurl><with id & chapterId>`);
setLoading(true);
const res = await axios.get(
`<someurl><with id & chapterId>`
);
setLoading(false);
console.log(res.data);
setChapters({
...chapters,
[chapterId]: res.data.chapter,
});
}
};
Book.js
useEffect(() => {
getBook(match.params.id);
// eslint-disable-next-line
}, []);
useEffect(() => {
if (Object.keys(book).length !== 0) {
setCurrentChapter(book.chapterIds[0]);
}
// eslint-disable-next-line
}, [book]);
useEffect(() => {
getChapter(currentChapter);
// eslint-disable-next-line
}, [currentChapter]);
Also, I get book.chapterIds as undefined on using it inside Book componentreturn()
What am I doing wrong?
Try to set all initial states as null:
const [book, setBook] = useState(null);
const [chapters, setChapters] = useState(null);
const [currentChapter, setCurrentChapter] = useState(null);
Then your useEffects:
useEffect(() => {
getBook(match.params.id);
// eslint-disable-next-line
}, []);
useEffect(() => {
if(book && book.chapterIds?.length > 0)
setCurrentChapter(book.chapterIds[0]);
// eslint-disable-next-line
}, [book]);
useEffect(() => {
if(currentChapter)
getChapter(currentChapter);
// eslint-disable-next-line
}, [currentChapter]);

How to organize async fetching code in Reactjs

In many components, I need to fetch some data and I'm ending up with a lot of similar code. It looks like this:
const [data, setData] = useState();
const [fetchingState, setFetchingState] = useState(FetchingState.Idle);
useEffect(
() => {
loadDataFromServer(props.dataId);
},
[props.dataId]
);
async function loadDataFromServer(id) {
let url = new URL(`${process.env.REACT_APP_API}/data/${id}`);
let timeout = setTimeout(() => setFetchingState(FetchingState.Loading), 1000)
try {
const result = await axios.get(url);
setData(result.data);
setFetchingState(FetchingState.Idle);
}
catch (error) {
setData();
setFetchingState(FetchingState.Error);
}
clearTimeout(timeout);
}
How can I put it into a library and reuse it?
Thank you guys for the suggestion, I came up with the following hook. Would be happy to some critics.
function useFetch(id, setData) {
const [fetchingState, setFetchingState] = useState(FetchingState.Idle);
useEffect(() => { loadDataFromServer(id); }, [id]);
async function loadDataFromServer(id) {
let url = new URL(`${process.env.REACT_APP_API}/data/${id}`);
let timeout = setTimeout(() => setFetchingState(FetchingState.Loading), 1000)
try {
const result = await axios.get(url);
setData(result.data);
setFetchingState(FetchingState.Idle);
}
catch (error) {
setData();
setFetchingState(FetchingState.Error);
}
clearTimeout(timeout);
}
return fetchingState;
}
And this is how I use it:
function Thread(props) {
const [question, setQuestion] = useState();
const fetchingState = useFetch(props.questionId, setQuestion);
if (fetchingState === FetchingState.Error) return <p>Error while getting the post.</p>;
if (fetchingState === FetchingState.Loading) return <Spinner />;
return <div>{JSON.stringify(question)}</div>;
}
You can wrap your APIs calls in /services folder and use it anywhere
/services
- Auth.js
- Products.js
- etc...
Example
Auth.js
import Axios from 'axios';
export const LoginFn = (formData) => Axios.post("/auth/login", formData);
export const SignupFn = (formData) => Axios.post("/auth/signup", formData);
export const GetProfileFn = () => Axios.get("/auth/profile")
in your component
import React, { useState } from 'react'
import { LoginFn } from '#Services/Auth'
export LoginPage = () => {
const [isLoading, setIsLoading] = useState(false);
const LoginHandler = (data) => {
setIsLoading(true)
LoginFn(data).then(({ data }) => {
// do whatever you need
setIsLoading(false)
})
}
return (
<form onSubmit={LoginHandler}>
.......
)
}

How do I use react custom hooks to make code reusable

I have two components similar like below:
const Login = props => {
let loading;
const dispatch = useDispatch();
const [notification, setNotification] = React.useState('');
const [hasNotification, setHasNotification] = React.useState('');
const [isLoading, setIsLoading] = React.useState(false);
const {status, message} = useSelector(state => state.LoginReducer);
const { register, handleSubmit, formState, errors } = useForm({
mode: "onChange"
});
const onSubmit = data => {
setIsLoading(true);
dispatch(loginStart(data));
};
React.useEffect(() => {
setIsLoading(false);
if (status === 422) {
setNotification(message);
setHasNotification('ERROR');
return;
}
if (status === 200) {
setNotification(message);
setHasNotification('SUCCESS');
}
}, [status, message]);
React.useEffect(() => {
console.log('componentDidMount');
return () => {
setNotification('');
setHasNotification('');
};
}, []);
return (
<AuthLayout title={'Login'} header={'Welcome back, Sign in'} hasNotification={hasNotification} notification={notification}>
</AuthLayout>
)
}
export default Login;
I also have another component with similar functionality as above
const Signup = props => {
let loading;
const dispatch = useDispatch();
const [notification, setNotification] = React.useState('');
const [hasNotification, setHasNotification] = React.useState('');
const [isLoading, setIsLoading] = React.useState(false);
const {status, message} = useSelector(state => state.SignupReducer);
const { register, handleSubmit, formState, errors } = useForm({
mode: "onChange"
});
const onSubmit = data => {
setIsLoading(true);
dispatch(signupStart(data));
};
React.useEffect(() => {
setIsLoading(false);
if (status === 422) {
setNotification(message);
setHasNotification('ERROR');
return;
}
if (status === 200) {
setNotification(message);
setHasNotification('SUCCESS');
}
}, [status, message]);
React.useEffect(() => {
console.log('componentDidMount');
return () => {
setNotification('');
setHasNotification('');
};
}, []);
return (
<AuthLayout title={'Signup'} header={'Discover a new way to do amazing work'} hasNotification={hasNotification} notification={notification}>
</AuthLayout>
)
}
export default Signup;
I read about custom hooks but just curious how I can move the state and logic to a separate custom hook function since they have similar structure and functionalities.
What will the custom hook look like?
You can declare all your state/hooks logic in a function and export it to your component:
Example: For your login component you can extract your logic to a file, let's call it useLogin.js
useLogin.js:
export default () => {
const [notification, setNotification] = React.useState('');
const [hasNotification, setHasNotification] = React.useState('');
const [isLoading, setIsLoading] = React.useState(false);
const { register, handleSubmit, formState, errors } = useForm({
mode: "onChange"
});
React.useEffect(() => {
setIsLoading(false);
if (status === 422) {
setNotification(message);
setHasNotification('ERROR');
return;
}
if (status === 200) {
setNotification(message);
setHasNotification('SUCCESS');
}
}, [status, message]);
React.useEffect(() => {
console.log('componentDidMount');
return () => {
setNotification('');
setHasNotification('');
};
}, []);
return [notification, hasNotification, setIsLoading]; //return all variable and functions that you need in your component
}
And in Login you should import your function and use it
import useLogin from './useLogin'; // first import useLogin function
const Login = props => {
let loading;
const dispatch = useDispatch();
const {status, message} = useSelector(state => state.LoginReducer);
const [notification, hasNotification, setIsLoading] = useLogin(); // call useLogin and get notification and hasNotification objects
const onSubmit = data => {
setIsLoading(true);
dispatch(loginStart(data));
};
return (
<AuthLayout title={'Login'} header={'Welcome back, Sign in'} hasNotification={hasNotification} notification={notification}>
</AuthLayout>
)
}
export default Login;
Same thing to Signup component
import useLogin from './useLogin';
const Signup = props => {
let loading;
const dispatch = useDispatch();
const {status, message} = useSelector(state => state.SignupReducer);
const [notification, hasNotification, setIsLoading] = useLogin();
const onSubmit = data => {
setIsLoading(true);
dispatch(signupStart(data));
};
return (
<AuthLayout title={'Signup'} header={'Discover a new way to do amazing work'} hasNotification={hasNotification} notification={notification}>
</AuthLayout>
)
}
export default Signup;
Hope the idea was clear;
You can create a new component with the same code, the difference is in the title and header from AuthLayout
<AuthLayout title={props.title} header={props.header} hasNotification={hasNotification} notification={notification}></AuthLayout>
Login
const Login = props => {
return (
<newComponent title={'Login'} header={'Welcome back, Sign in'} />
)
}
export default Login;
SignUp
const SignUp = props => {
return (
<newComponent title={'SignUp'} header={'Discover a new way to do amazing work'} />
)
}
export default SignUp;
I called newComponent, the component that you will create

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