I am using Create-React-App and the (excellent) use-http for a custom useFetch hook. The goal is to make several API calls upon login to an account area:
const [user, setUser] = useState(null)
const [profile, setProfile] = useState(null)
const [posts, setPosts] = useState(null)
const request = useFetch('/')
const initializeAccount = async () => {
try {
const user = await request.get('api/user/')
const profile = await request.get('api/profile/')
const posts = await request.get('api/posts/')
if (user) {
setUser(user.data)
}
if (profile) {
setProfile(profile.data)
}
if (posts) {
setPosts(posts.data)
}
} catch (e) {
console.log('could not initialize account')
}
}
useEffect(() => {
initializeAccount()
return () => console.log('unmount')
})
I have tried using [] as the dependency array, but I get a linting error saying to move initializeAccount to the dependency array. If I add it, the function runs endlessly.
What is the correct way to setup the dependency array so that this function is called one time? Also, what would be the correct way to handle abort of each of the API calls in this scenario?
My man, in order to run useEffect once for api calls, you have to do it like this:
useEffect(() => {
initializeAccount()
return () => console.log('unmount')
},[])
Hope it helps.
Related
I'm trying to use a hook inside of a useEffect call to run only once (and load some data).
I keep getting the error that I can't do that (even though I've done the exact same thing in another app, not sure why 1 works and the other doesn't), and I understand I may be breaking the Rules of Hooks... so, what do I do instead? My goal was to offload all the CRUD operation logic into a simple hook.
Here's MenuItem, the component trying to use the hook to get the data.
const MenuItem = () => {
const [ID, setID] = useState<number | null>(null);
const [menu, setMenu] = useState<Item[]>([]);
const { getMenu, retrievedData } = useMenu();
//gets menu items using menu-hook
useEffect(() => {
getMenu();
}, []);
//if menu is retrieved, setMenu to retrieved data
useEffect(() => {
if (retrievedData.length) setMenu(retrievedData);
}, []);
//onClick of menu item, displays menu item description
const itemHandler = (item: Item) => {
if (ID === null || ID !== item._id) {
setID(item._id);
} else {
setID(null);
}
};
return ...
};
And here's getMenu, the custom hook that handles the logic and data retrieval.
const useMenu = () => {
const backendURL: string = 'https://localhost:3001/api/menu';
const [retrievedData, setRetrievedData] = useState<Item[]>([]);
const getMenu = async () => {
await axios
.get(backendURL)
.then((fetchedData) => {
setRetrievedData(fetchedData.data.menu);
})
.catch((error: Error) => {
console.log(error);
setRetrievedData([]);
});
};
return { getMenu, retrievedData };
};
export default useMenu;
And finally here's the error.
Invalid hook call. Hooks can only be called inside of the body of a function component.
I'd like to add I'm also using Typescript which isn't complaining right now.
There's a few things you can do to improve this code, which might help in future. You're right that you're breaking the rule of hooks, but there's no need to! If you move the fetch out of the hook (there's no need to redefine it on every render) then it's valid not to have it in the deps array because it's a constant.
I'd also make your useMenu hook take care of all the details of loading / returning the loaded value for you.
const fetchMenu = async () => {
const backendURL: string = 'https://localhost:3001/api/menu';
try {
const { data } = await axios.get(backendURL);
return data.menu;
} catch (error: AxiosError) {
console.log(error);
return [];
};
}
export const useMenu = () => {
const [items, setItems] = useState<Item[]>([]);
useEffect(() => {
fetchMenu.then(result => setItems(result);
}, []);
return items;
};
Now you can consume your hook:
const MenuItem = () => {
const [ID, setID] = useState<number | null>(null);
// Now this will automatically be an empty array while loading, and
// the actual menu items once loaded.
const menu = useMenu();
// --- 8< ---
return ...
};
A couple of other things -
Try to avoid default exports, because default exports are terrible.
There are a lot of packages you can use to make your life easier here! react-query is a good one to look at as it will manage all the lifecycle/state management around external data
Alternatively, check out react-use, a collection of custom hooks that help deal with lots of common situations like this one. You could use the useAsync hook to simplify your useMenu hook above:
const backendURL: string = 'https://localhost:3001/api/menu';
const useMenu = () => useAsync(async () => {
const { data } = await axios.get(backendURL);
return data.menu;
});
And now to consume that hook:
const MenuItem = () => {
const { value: menu, loading, error } = useMenu();
if (loading) {
return <LoadingIndicator />;
}
if (error) {
return <>The menu could not be loaded</>;
}
return ...
};
As well as being able to display a loading indicator while the hook is fetching, useAsync will not give you a memory leak warning if your component unmounts before the async function has finished loading (which the code above does not handle).
After working on this project for some time I've also found another solution that is clean and I believe doesn't break the rule of hooks. This requires me to set up a custom http hook that uses a sendRequest function to handle app wide requests. Let me make this clear, THIS IS NOT A SIMPLE SOLUTION, I am indeed adding complexity, but I believe it helps since I'll be making multiple different kinds of requests in the app.
This is the sendRequest function. Note the useCallback hook to prevent unnecessary rerenders
const sendRequest = useCallback(
async (url: string, method = 'GET', body = null, headers = {}) => {
setIsLoading(true);
const httpAbortCtrl = new AbortController();
activeHttpRequests.current.push(httpAbortCtrl);
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal,
});
const responseData = await response.json();
activeHttpRequests.current = activeHttpRequests.current.filter(
(reqCtrl) => reqCtrl !== httpAbortCtrl
);
if (!response.ok) throw new Error(responseData.message);
setIsLoading(false);
return responseData;
} catch (error: any) {
setError(error);
setIsLoading(false);
throw error;
}
},
[]
);
Here's the new useMenu hook, note I don't need to return getMenu as every time sendRequest is used in my app, getMenu will automatically be called.
export const useMenu = () => {
const { sendRequest } = useHttpClient();
const [menu, setMenu] = useState<MenuItem[]>([]);
const [message, setMessage] = useState<string>('');
useEffect(() => {
const getMenu = async () => {
try {
const responseData = await sendRequest(`${config.api}/menu`);
setMenu(responseData.menu);
setMessage(responseData.message);
} catch (error) {}
};
getMenu();
}, [sendRequest]);
return { menu, message };
};
Good luck
I get data from backend and set to my state in componentdidmount but value not set after log state
const [tasks, setTasks] = useState([]);
const getTasks = async () => {
const getTodoInformation = {
email: localStorage.getItem("tokenEmail"),
};
if (getTodoInformation.email) {
const response = await axios.post(
"http://localhost:9000/api/todo/get",
getTodoInformation
);
setTasks(response.data.data);
}
};
useEffect(() => {
getTasks();
console.log(tasks);
}, []);
My tasks is empty when i log it
So the title and the question itself are actually two questions.
React Hook useEffect has a missing dependency: 'tasks'. Either includes it or remove the dependency array
That's because you include a state (i.e. tasks) in the useEffect hook. And React is basically asking you, "Do you mean run console.log(tasks) every time tasks is updated?". Because what you are doing is run the useEffect hook once and only once.
And for your "actual" question
value not set after log state
In short, states are set in async manner in React. That means tasks is not necessary immediately updated right after you call setTasks. See #JBallin comment for details.
const [tasks, setTasks] = useState([]);
useEffect(() => {
setTimeout(async () => {
const getTodoInformation = {
email: localStorage.getItem("tokenEmail"),
};
if (getTodoInformation.email) {
const response = await axios.post(
"http://localhost:9000/api/todo/get",
getTodoInformation
);
setTasks(response.data.data);
}
}, 1000);
console.log(tasks);
}, []);
The main problem is that useEffect -> is a sync method, getTasks() is asynchronous, and useEffect only works once when your component mounts. Shortly speaking, you got your data from the backend after useEffect worked.
For example, if you will add one more useEffect
useEffect(() => {
console.log(tasks);
}, [tasks]);
You will see log, after your data will have changed.
You can use self-calling async function inside useEffect as shown here:
const [tasks, setTasks] = useState([]);
const getTasks = async () => {
const getTodoInformation = {
email: localStorage.getItem("tokenEmail"),
};
if (getTodoInformation.email) {
const response = await axios.post(
"http://localhost:9000/api/todo/get",
getTodoInformation
);
return response.data.data;
}
};
useEffect(() => {
(async () => {
const tasks = await getTasks();
setTasks(tasks);
})();
console.log(tasks);
}, [tasks]);
I've been studying react hook lately. I found the following in the book I saw.
It is said that the fetchAndSetUser function is updated only when the userId is changed using the useCallback hook.
function Profile({ userId }) {
const [user, setUser] = useState();
const fetchAndSetUser = useCallback(
async needDetail => {
const data = await fetchUser(userId, needDetail);
setUser(data);
},
[userId]
);
useEffect(() => {
fetchAndSetUser(false);
} , [fetchAndSetUser]);
// ...
However, if the fetchAndSetUser function is updated only when the userId is changed using the useCallback hook, I wonder what the difference is from just putting the userId in the dependency array in the useEffect hook. (There is a code below.)
function Profile({ userId }) {
const [user, setUser] = useState();
const fetchAndSetUser = async ( needDetail ) => {
const data = await fetchUser(userId, needDetail);
setUser(data);
};
useEffect(() => {
fetchAndSetUser(false);
} , [userId]);
// ...
I wonder if the two codes are the same or if they are different.
Even though I think both codes would achieve the same result, there is a small difference.
If you wanted to pass that fetchAndSetUser (with no useCallback) function to the children, any time Profile component updated it would also update the children.
You can always combine all with:
function Profile({ userId }) {
const [user, setUser] = useState();
const fetchAndSetUser = useCallback(async ( needDetail ) => {
const data = await fetchUser(userId, needDetail);
setUser(data);
}, [userId]);
useEffect(() => {
fetchAndSetUser(false);
} , [userId]);
I found many similar questions here about React Hook useEffect has a missing dependency. I have already checked them, but I didn't find solutions as I faced. I want to pass redux thunk function as a parameter to React custom hook.
Below is my code and it is working fine. But, I got dependency missing warning, I don't want to add ignore warning eslint. If I add dispatchAction to dependency array list, it is dispatching again and again because redux thunk asyn function has fulfilled, reject, pending.
Custom Hook
const useFetchData = (dispatchAction, page) => {
const dispatch = useDispatch();
const [loadMoreLoading, setLoadMoreLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState();
useEffect(() => {
const fetchData = async () => {
setLoadMoreLoading(true);
const resultAction = await dispatch(dispatchAction);
if (resultAction.meta.requestStatus === 'rejected') {
setErrorMsg(resultAction.payload.message);
}
setLoadMoreLoading(false);
};
fetchData();
}, [dispatch, page]);
return [loadMoreLoading, errorMsg]; // it is asking for adding dispatchAction.
My component
const SomeListing = ({userId}) => {
const [page, setPage] = useState(1);
const [loadMoreLoading, errorMsg] = useFetchData(
fetchPropertyByUserId({userId: userId, page: page}),
page,
);
}
So, is there any way to be able to add redux thunk function in react custom hook?
The function fetchPropertyByUserId, when called i.e. fetchPropertyByUserId({userId: userId, page: page}), returns an "actionCreator" function.
Hence, when you call this function at the place of first parameter of your hook useFetchData, it returns a new "actionCreator" function each time (we know that hooks are called at each render):
In SomeListing.jsx:
const [loadMoreLoading, errorMsg] = useFetchData(
fetchPropertyByUserId({userId: userId, page: page}), // <-- Here: it returns a new "actionCreator" function at call (render)
page,
);
And, as soon as you put this function (first parameter of the hook i.e. dispatchAction) as a dependency of useEffect, it should cause an infinite execution of the effect because, now we know, that dispatchAction is getting created (hence, changed) at every render.
In useFetchData.js:
export const useFetchData = (dispatchAction, page) => {
// ...
useEffect(() => {
const fetchData = async () => {
setLoadMoreLoading(true)
const resultAction = await dispatch(dispatchAction)
if (resultAction.meta.requestStatus === 'rejected') {
setErrorMsg(resultAction.payload.message)
}
setLoadMoreLoading(false)
}
fetchData()
}, [dispatch, dispatchAction, page]) // <-- "dispatchAction" added here
// ...
How to fix it?
Pass a memoized actionCreator function:
In SomeListing.jsx:
export const SomeListing = ({ userId }) => {
const [page, setPage] = useState(1)
// Here: "fetchPropertyByUserIdMemo" is memoized now
const fetchPropertyByUserIdMemo = useMemo(
() => fetchPropertyByUserId({ userId: userId, page: page }),
[page, userId]
)
const [loadMoreLoading, errorMsg] = useFetchData(fetchPropertyByUserIdMemo, page)
// ...
}
How about extracting the fetch method from useEffect?:
const fetchData = async () => {
setLoadMoreLoading(true);
const resultAction = await dispatch(dispatchAction);
if (resultAction.meta.requestStatus === 'rejected') {
setErrorMsg(resultAction.payload.message);
}
setLoadMoreLoading(false);
};
useEffect(() => {
fetchData();
}, [fetchData]);
My component gets the hashed-id from the query string, then calls api with that hash to fetch a post for review.
eslint forces me to add my custom hook to dependency array.
fetchpost();
}, [query]);
But doing this causes an infinite loop. In order to stop it I need to disable this eslint rule, as seen below.
// component file
const history = useHistory();
const dispatch = useDispatch();
const query = useQuery();
const [post, setPost] = useState(null);
const [hash, setHash] = useState(null);
useEffect(() => {
const fetchpost = async () => {
const hash = query.get("hashed_id");
const post = await fetchReviewPost(
`/api/posts/${hash}/review`
);
setHash(hash);
setPost(post);
};
fetchpost();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// utils file
import { useLocation } from "react-router-dom";
export const getCurrentURL = () => {
return document.URL;
};
export const useQuery = () => {
const queryString = useLocation().search;
return new URLSearchParams(queryString);
};
Dan Abramov writes An infinite loop may also happen if you specify a value that always changes in the dependency array.
Is that the case here? Is query reference different on every render? And why eslint wants to put it in a dependency array?
He also says removing a dependency you use (or blindly specifying []) is usually the wrong fix. Which I sort of did by disabling the eslint rule.
Any thoughts?
If you really want to keep sticking to eslint suggestions and using the useQuery hook, here is an alternative way:
// component file
const history = useHistory();
const dispatch = useDispatch();
const q = useQuery();
const [query] = useState(q);
const [post, setPost] = useState(null);
const [hash, setHash] = useState(null);
useEffect(() => {
const fetchpost = async () => {
const hash = query.get("hashed_id");
const post = await fetchReviewPost(
`/api/posts/${hash}/review`
);
setHash(hash);
setPost(post);
};
fetchpost();
}, [query]);
At this point the query value keeps constant across the subsequent function calls.
However, I'd remove the useQuery hook, and place its content straight into the fetchpost function.