useEffect not seeing dependency change on single click - reactjs

I am making a call to an Api using the following hook. It returns 10 pictures at a time.
export const useFetchData = (url, page) => {
const [error, setError] = useState(null)
const [apiData, setApiData] = useState(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
const res = await axios.get(*****);
const data = await res.data
setApiData(data)
} catch (e) {
setError(e)
} finally {
setLoading(false)
}
}
fetchData()
}, [page, url])
return { apiData, loading, error }
}
I am trying to do pagination in the following component by changing the state value of page by using the nextPage and backPage functions.
let [page, setPage] = useState(1);
let { apiData, loading, error } = useFetchData(url, page);
const nextPage = () => {
setPage(page ++);
};
const backPage = () => {
setPage(page --);
};
return (
<div className="photo-display__buttons-container">
<button onMouseDown={()=>backPage()}>Back</button>
<button onClick={()=>nextPage()}>Next</button>
</div>
<main className="photo-display">
<div className="photo-display__container">
{apiData?.photos.map((photo) => (
<Photo key={photo.id} photo={photo} />
))}
</div>
</div>
);
};
export default App;
By extensive console logging I am able to see that the state value is changed and the hook is called but the try catch does not execute on a single click.
Only if it is double clicked does the try catch execute. The state value is temporarily changed to reflect the double increase but after the hook is called in goes back to the correct value.
Why? and How do i get it to work on a single click?

When you do setPage you are using a postfix ++, which means the original value will be returned (and then incremented). You need to use a prefix ++ so that it is incremented first, then passed in to setState, or just skip the ++ entirely and do setState(i + 1).
Eg (postfix):
let i = 0;
console.log(i++);
Eg (prefix):
let i = 0;
console.log(++i);

Try changing setPage(page ++) to setPage(page+1).

Related

How to get rid of this infinite loop using useEffect and useCallback?

I wanna load the first batch of comments immediately (using useEffect) and then load additional pages when a "load more" button is pressed.
The problem is that my current setup causes an infinite loop (caused by the dependency on comments).
If I remove the fetchNextCommentsPage function from the useEffect dependency list, everything seems to work, but EsLint complains about the missing dependency.
const [comments, setComments] = useState<CommentModel[]>([]);
const [commentsLoading, setCommentsLoading] = useState(true);
const [commentsLoadingError, setCommentsLoadingError] = useState(false);
const [paginationEnd, setPaginationEnd] = useState(false);
const fetchNextCommentsPage = useCallback(async function () {
try {
setCommentsLoading(true);
setCommentsLoadingError(false);
const continueAfterId = comments[comments.length - 1]?._id;
const response = await BlogApi.getCommentsForBlogPost(blogPostId, continueAfterId);
setComments([...comments, ...response.comments]);
setPaginationEnd(response.paginationEnd);
} catch (error) {
console.error(error);
setCommentsLoadingError(true);
} finally {
setCommentsLoading(false);
}
}, [blogPostId, comments])
useEffect(() => {
fetchNextCommentsPage();
}, [fetchNextCommentsPage]);
Never put the state you want to mutate in the dependencies list as it will always raise an infinite loop issue.
The common way to solve this is to use the callback function of setState https://reactjs.org/docs/react-component.html#setstate.
If you want your effect triggered only once, put something that never changes after the first loading.
When you want to load more when pressing a button, just change the dependencies of your effect to run your effect again with new dependency value.
const [comments, setComments] = useState<CommentModel[]>([]);
const [commentsLoading, setCommentsLoading] = useState(true);
const [commentsLoadingError, setCommentsLoadingError] = useState(false);
const [continueAfterId, setContinueAfterId] = useState(null)
const [paginationEnd, setPaginationEnd] = useState(false);
const fetchNextCommentsPage = useCallback(async function () {
try {
setCommentsLoading(true);
setCommentsLoadingError(false);
const response = await BlogApi.getCommentsForBlogPost(blogPostId, continueAfterId);
setComments(previousState => [...previousState, ...response.comments]);
setPaginationEnd(response.paginationEnd);
} catch (error) {
console.error(error);
setCommentsLoadingError(true);
} finally {
setCommentsLoading(false);
}
}, [blogPostId, continueAfterId]);
useEffect(() => {
fetchNextCommentsPage();
}, [fetchNextCommentsPage]);
const onButtonPressed = useCallback(() => {
// continueAfterId is one of the dependencies of fetchNextCommentsPage so it will change `fetchNextCommentsPage`, hence trigger the effect
setContinueAfterId(comments[comments.length - 1]?._id)
}, [comments])
Thank you to #Đào-minh-hạt for their answer. I improved it by passing the continueAfterId as an argument, rather than holding it in another state (which, I think, is more intuitive):
const [comments, setComments] = useState<CommentModel[]>([]);
const [commentsLoading, setCommentsLoading] = useState(true);
const [commentsLoadingError, setCommentsLoadingError] = useState(false);
const [paginationEnd, setPaginationEnd] = useState(false);
const fetchNextCommentsPage = useCallback(async function (continueAfterId?: string) {
try {
setCommentsLoading(true);
setCommentsLoadingError(false);
const response = await BlogApi.getCommentsForBlogPost(blogPostId, continueAfterId);
setComments(existingComments => [...existingComments, ...response.comments]);
setPaginationEnd(response.paginationEnd);
} catch (error) {
console.error(error);
setCommentsLoadingError(true);
} finally {
setCommentsLoading(false);
}
}, [blogPostId]);
useEffect(() => {
fetchNextCommentsPage();
}, [fetchNextCommentsPage]);
And then in my load-more button's onClick:
<Button
variant="outline-primary"
onClick={() => fetchNextCommentsPage(comments[comments.length - 1]?._id)}>
Load more comments
</Button>

Can not change state of object [duplicate]

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 2 years ago.
I am trying to fetch a single object, and it fetches, I get correct data in response.data, but i can't set it to state. It just remains as a null object. What am I doing wrong?
Post is a json object with several fields like: post_id, post_title, post_content and etc.
const [post, setPost] = useState({})
let id = match.params.id
useEffect(() => {
axios.get(`${BASE_URL}/post?id=${id}`)
.then(response => {
console.log(response.data)
setPost(response.data)
})
.then(() => {
console.log("post: ", post)
}
)
setAction like your setPost are asynchronous, as stated in the official REACT documentation (https://reactjs.org/docs/hooks-reference.html#usestate); this means that, once the setAction is executed, you don't know when it will be actually executed and terminated: you will know because the component will re-render.
In your case, if you'd like to perform action AFTER post has got the new value, you would need the useEffect hook (https://reactjs.org/docs/hooks-reference.html#useeffect):
React.useEffect(() => {
console.log(post);
}, [post]);
By the way, I think you would want the data inside the response, so you would probably save the JSON retrieved from the body of the HTTP Response, that you can get using response.json().
EDIT: As stated in the comment from Siradji Awoual, what I wrote about response and response.json() is not valid for Axios (but it still is for fetch API).
Setting a state is asynchronous. That means you don't know exactly when that action will finish executing.
If I were you, I would use something like useEffect to check if the state is being set.
React.useEffect(() => console.log(post), [post])
Using axios.get is low-level and requires that you hook up a bunch of extra stuff to get things working correctly. Instead, try writing custom hooks to abstract this logic away -
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [result, setResult] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
Using useAsync looks like this -
const MyApp = () => {
const { loading, error, result } =
useAsync(_ => axios.get("./foo.json").then(res => res.json()))
if (loading)
return <p>loading...</p>
if (error)
return <p>error: {error.message}</p>
return <pre>result: {result}</pre>
}
But you will probably have many components that fetch JSON, right? We can make an even higher level custom hook, useJSON that is a specialization of useAsync -
const fetchJson = (url = "") =>
axios.get(url).then(r => r.json()) // <-- stop repeating yourself
const useJson = (url = "") =>
useAsync(fetchJson, [url]) // <-- useAsync
const MyApp = () => {
const { loading, error, result } =
useJson("./foo.json") // <-- dead simple
if (loading)
return <p>loading...</p>
if (error)
return <p>error: {error.message}</p>
return <pre>result: {result}</pre>
}
See the custom hooks in action in this functioning code snippet -
const { useState, useEffect } =
React
// fake fetch slows response down so we can see loading
const _fetch = (url = "") =>
fetch(url).then(x =>
new Promise(r => setTimeout(r, 2000, x)))
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [result, setResult] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
const fetchJson = (url = "") =>
_fetch(url).then(r => r.json())
const useJson = (url = "") =>
useAsync(fetchJson, [url])
const MyComponent = ({ url = "" }) => {
const { loading, error, result } =
useJson(url)
if (loading)
return <pre>loading...</pre>
if (error)
return <pre style={{color: "tomato"}}>error: {error.message}</pre>
return <pre>result: {JSON.stringify(result, null, 2)}</pre>
}
const MyApp = () =>
<main>
ex 1 (success):
<MyComponent url="https://httpbin.org/get?foo=bar" />
ex 2 (error):
<MyComponent url="https://httpbin.org/status/500" />
</main>
ReactDOM.render(<MyApp />, document.body)
pre {
background: ghostwhite;
padding: 1rem;
white-space: pre-wrap;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

async custom hooks does't provide updated data

I have a simple hook to help me handle a POST request. With the following code, I expect unsub will be true after the POST is done. Can anyone point out anything I could have done wrong?
Custom Hook
const useUnsubscribeEmail = () => {
const [userId, setUserId] = useState(null);
const [unsub, setUnSub] = useState();
const UNSUB_URL = '/web-registry-api/v1/reviews/unsubscription';
useEffect(() => {
if (userId) {
// async POST call
(async () => {
try {
await ApiService.post(`${UNSUB_URL}/${userId}`);
// update unsub value
setUnSub(true);
} catch (error) {
console.error(error)
}
})();
}
}, [userId]);
return [unsub, setUserId];
};
export default useUnsubscribeEmail;
Component
const ReviewUnsubscription = () => {
const { userId } = useParams();
const [unsub, unsubscribeEmail] = useUnsubscribeEmail();
return (
<MinimumLayout>
<div className={styles.content}>
<h1>Unsubscribe from email reminders to review products you’ve received from Zola?{unsub}</h1>
{/* unsub here is still undefined */}
<Button disabled={unsub} onClick={() => { unsubscribeEmail(userId); }} variant="primary" className={styles.button}>Unsubscribe</Button>
</div>
</MinimumLayout>
);
};
unsub is still going to be undefined until you click the button as you have not set a default state for it in your hook.
change : const [unsub, setUnSub] = useState(); to const [unsub, setUnSub] = useState(false); is what I would recommend
I tested on my side and works just fine; However, I cannot test the APIService.post.

Proper use of UseCallBack

Currently, my code re-renders every time the query parameter is updated. Once I remove the query parameter; however, I get a warning stating "React Hook useCallback has a missing dependency: 'query'. Either include it or remove the dependency array react-hooks/exhaustive-deps". I have tried just defining my getData function within the useEffect, but I am using getData as on onclick function outside of the useEffect. What I am trying to accomplish is to initially fetch articles on react hooks and then only fetch new data on submit as opposed to when the query is updated and not have any warnings about query being a missing dependency as well. Any suggestions would help immensely. the code is as follows:
import React, { useState, useEffect, useCallback } from "react"
import axios from "axios"
const Home = () => {
const [data, setData] = useState(null)
const [query, setQuery] = useState("react hooks")
const getData = useCallback(async () => {
const response = await axios.get(
`http://hn.algolia.com/api/v1/search?query=${query}`
)
setData(response.data)
}, [query])
useEffect(() => {
getData()
}, [getData])
const handleChange = event => {
event.preventDefault()
setQuery(event.target.value)
}
return (
<div>
<input type='text' onChange={handleChange} value={query} />
<button type='button' onClick={getData}>
Submit
</button>
{data &&
data.hits.map(item => (
<div key={item.objectID}>
{item.url && (
<>
<a href={item.url}>{item.title}</a>
<div>{item.author}</div>
</>
)}
</div>
))}
</div>
)
}
export default Home
Add a submitting state as a condition for triggering your axios request
const [submitting, setSubmitting] = useState(true)
const [data, setData] = useState(null)
const [query, setQuery] = useState("react hooks")
useEffect(() => {
const getData = async () => {
const response = await axios.get(
`http://hn.algolia.com/api/v1/search?query=${query}`
)
setData(response.data)
setSubmitting(false) // call is finished, set to false
}
// query can change, but don't actually trigger
// request unless submitting is true
if (submitting) { // is true initially, and again when button is clicked
getData()
}
}, [submitting, query])
const handleChange = event => {
event.preventDefault()
setQuery(event.target.value)
}
const getData = () => setSubmitting(true)
If you wanted to useCallback, it could be refactored as such:
const [submitting, setSubmitting] = useState(true)
const [data, setData] = useState(null)
const [query, setQuery] = useState("react hooks")
const getData = useCallback(async () => {
const response = await axios.get(
`http://hn.algolia.com/api/v1/search?query=${query}`
)
setData(response.data)
}, [query])
useEffect(() => {
if (submitting) { // is true initially, and again when button is clicked
getData().then(() => setSubmitting(false))
}
}, [submitting, getData])
const handleChange = event => {
event.preventDefault()
setQuery(event.target.value)
}
and in render
<button type='button' onClick={() => setSubmitting(true)}>

useEffect within a custom hook doesn't rerender on delete

I am trying to use hooks and implement a custom hook for handling my data fetching after every update I send to the API.
My custom hook, however, doesn't fire on change like I want it too. Delete has to be clicked twice for it to rerender. Note: I removed some functions from this code as they don't pertain to the question.
import React, { useState, useEffect } from "react"
import {Trash} from 'react-bootstrap-icons'
import InlineEdit from 'react-ions/lib/components/InlineEdit'
function Board(){
const [render, setRender] = useState(false)
const [boards, setBoards] = useState([]);
const [isEditing, setEdit] = useState(false)
const [value, setValue] = useState("")
const[newValue, setNewValue] = useState("")
const [error, setError] = useState("")
function useAsyncHook(setState, trigger) {
const [result] = useState([]);
const [loading, setLoading] = useState("false");
useEffect(() => {
async function fetchList() {
try {
setLoading("true");
const response = await fetch(
`http://localhost:8080/api/boards`
);
const json = await response.json();
setState(json)
} catch (error) {
//console.log(error)
setLoading("null");
}
}
fetchList()
}, [trigger]);
return [result, loading];
}
useAsyncHook(setBoards, render)
const handleDelete = (id) => {
console.log("delete clicked")
setLoading(true);
fetch(`http://localhost:8080/api/boards/` + id, {
method: "DELETE",
headers: {
"Content-Type": "application/json"
},
})
setRender (!render)
}
return(
<div>
<ul>
{boards.map(board => (
<li key={board.id}>
<InlineEdit value={board.size} isEditing={isEditing} changeCallback={(event)=>handleSave (event, board.id)} />
<Trash onClick={()=>handleDelete(board.id)}/>
</li>
</ul>
</div>
);
}
export default Board
OPTION 1:
Maybe you wanna have a hook that tells you when to fetch the board, right? For example:
const [auxToFetchBoard, setAuxToFetchBoard] = useState(false);
Then, in a useEffect you execute the function fetchBoard everytime that hook changes:
useEffect(fetchBoard, [auxToFetchBoard]);
Finally, in your handleDelete function, if your delete request returns correctly, you have to update auxToFetchBoard. Something like this:
const handleDelete = (id) => {
setIsLoading(true);
setError("");
fetch(yourURL, yourOptions)
.then(res => {
// check if response is correct and
setIsLoading(false);
setAuxToFetchBoard(!auxToFetchBoard);
})
.catch(() => {
setIsLoading(false);
setError("Error while deleting stuff");
});
};
Note: I changed the names of isLoading and setIsLoading because they are more explicit.
OPTION 2:
Instead of fetching the board again and again, you can update your board (in this case your code would be in 8th line inside the handleDeletefunction).
Hope it helps.

Resources