When does the callback function inside the useCallback() hook runs exactly? - javascript

Here is the code for infinite scroll load
Mainly two components MainComponent and a custom hook component
everytime i entered something on search item it sends the request and display the data to screen and inside main component i am using lastELementRef to set observer Api on that to send the request again when i scrolled at the end .
Not able to understand when does function passed inside useCallBack(()=>{}) runs
to check how many times it runs i did console.log at line no 21 inside MainComponent.
It will be very nice of folks on this community if anybody can explain me when does it runs.
I have googled and watched some Youtube videos on useCallback and all I can come up with is that it gives the function object only when the dependency inside its dependency array changes else on it memoizes the function on each re-render if dependency does not change.?
i am sharing the code here
have used axios to send request.
//MainComponent
import React,{useState,useRef,useCallback} from 'react'
import useBookSearch from './useBookSearch';
export default function MainComponent() {
//these 2 stataes are here because
//we want them to be used in this component only
//meaning we dont want them to be part
//of any custom logic
const[query,setQuery] = useState('');
const[pageNumber,setPageNumber] = useState(1);
const observer = useRef();
const {books,loading,hasMore,error} = useBookSearch(query,pageNumber);
const lastElementRef = useCallback(node=>{
console.log("How many times did i run ?");
if(loading) return ;
if(observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries=>{
if(entries[0].isIntersecting && hasMore){
setPageNumber(prevPage => prevPage + 1);
}
})
if(node) observer.current.observe(node);
console.log(node);
},[loading,hasMore])
const handleSearch=(e)=>{
setQuery(e.target.value);
setPageNumber(1);
}
return (
<div>
<input value={query} type="text" onChange={handleSearch} ></input>
{books.map((book,index) =>{
if(books.length === index + 1)
{
return <div ref={lastElementRef}key={book}>{book}</div>
}
else{
return <div key={book}>{book}</div>
}
})}
{loading && 'Loading.....................'}
{error && 'Error........'}
</div>
)
}
//custom hook component-useBookSearch
import { useEffect,useState } from 'react';
import axios from 'axios'
export default function useBookSearch(query,pageNumber) {
const[loading,setLoading] = useState('true');
const[error,setError] = useState('false');
const[books,setBooks] = useState([]);
const[hasMore,setHasMore] = useState(false);
//second useEffect which clear the books first
//and then make an api request
useEffect(()=>{
setBooks([]);
},[query])
useEffect(()=>{
setLoading(true);
setError(false);
let cancel ;
axios({
method:'GET',
url:'http://openlibrary.org/search.json',
params:{q:query,page:pageNumber},
cancelToken:new axios.CancelToken(c=>cancel=c)
}).then(res=>{
setBooks(prevBooks=>{
return [...new Set([...prevBooks,...res.data.docs.map(b=>b.title)])]
})
setHasMore(res.data.docs.length > 0);
setLoading(false);
console.log(res.data);
}).catch(e=>{
if(axios.isCancel(e)) return
setError(true);
})
return ()=> cancel();
},[query,pageNumber])
return {loading,error,books,hasMore};
}
screenshot of how the code looks when i entered the string test to fetch data
Screenshot of the console window when entering test into input box

Related

Re-render List after deleting item in child component

I want to build a dashboard for a blog. I have a page, listing all blog posts using a component for each list item. Now, inside each list item, I have a button to delete the post.
So far, everything is working. The post gets deleted, and if I reload the page, it is gone from the list. But I can't get it to re-render the page automatically, after deleting a post. I kind of cheated here using window.location.reload() but there has to be a better way?
This is my Page to build the list of all Posts
import {
CCol,
CContainer,
CRow,
CTable,
CTableHead,
CTableRow,
CTableHeaderCell,
CTableBody,
} from "#coreui/react";
import { useState, useEffect } from "react";
import DashboardSidebar from "../../components/dashboard/Sidebar";
import { getAllBlogPosts } from "../../services/blogService";
import BlogListItem from "../../components/dashboard/blog/BlogListItem";
import "./Dashboard.scss";
const AdminBlogListView = () => {
const [blogposts, setBlogposts] = useState([]);
useEffect(() => {
getBlogPosts();
}, []);
async function getBlogPosts() {
const response = await getAllBlogPosts();
setBlogposts(response.data);
}
// console.log(blogposts);
return (
<div className="adminContainer">
<div className="adminSidebar">
<DashboardSidebar />
</div>
<div className="adminContent">
<CContainer fluid>
<CRow className="mb-3">
<CCol>
<CTable>
<CTableHead>
<CTableRow>
<CTableHeaderCell scope="col">#</CTableHeaderCell>
<CTableHeaderCell scope="col">Titel</CTableHeaderCell>
<CTableHeaderCell scope="col">Content</CTableHeaderCell>
<CTableHeaderCell scope="col"></CTableHeaderCell>
</CTableRow>
</CTableHead>
<CTableBody>
{blogposts.map((post) => {
return <BlogListItem key={post._id} post={post} />;
})}
</CTableBody>
</CTable>
</CCol>
</CRow>
</CContainer>
</div>
</div>
);
};
export default AdminBlogListView;
And this is the BlogListItem Component
import { useState, useEffect } from "react";
import {
CTableRow,
CTableHeaderCell,
CTableDataCell,
} from "#coreui/react";
import CIcon from "#coreui/icons-react";
import * as icon from "#coreui/icons";
import {
deleteBlogPost,
getBlogPostById,
// updateBlogPost,
} from "../../../services/blogService";
import { useNavigate } from "react-router-dom";
const BlogListItem = (props) => {
const id = props.post._id;
const [visible, setVisible] = useState(false);
const [post, setPost] = useState({
title: "",
content: "",
});
useEffect(() => {
getBlogPostById(id)
.then((response) => setPost(response.data))
.catch((error) => console.log(error));
}, []);
const handleDelete = async (event) => {
event.preventDefault();
const choice = window.confirm("Are you sure you want to delete this post?");
if (!choice) return;
await deleteBlogPost(post._id);
window.location.reload();
};
return (
<>
<CTableRow>
<CTableHeaderCell scope="row">1</CTableHeaderCell>
<CTableDataCell>{post.title}</CTableDataCell>
<CTableDataCell>{post.content}</CTableDataCell>
<CTableDataCell>
<CIcon
icon={icon.cilPencil}
size="lg"
onClick={() => setVisible(!visible)}
/>
<CIcon
icon={icon.cilTrash}
className="deleteButton"
size="lg"
color=""
onClick={handleDelete}
/>
</CTableDataCell>
</CTableRow>
</>
);
};
export default BlogListItem;
What can I do instead of window.location.reload() to render the AdminBlogListView after deleting an item? I tried using useNavigate() but that doesn't do anything
Thanks in advance :)
You can pass a reference to a function from the parent component AdminBlogListView into the child component BlogListItem, such that it is invoked when a blog post is deleted. That function will have the effect of either repopulating the blog posts or manually removing it from the data (that implementation bit is up to you).
Solution 1: Repopulate all blog posts on deletion
This is a quick fix with a bit of code smell (because you're essentially querying the server twice: once to delete the post and another to fetch posts again). However it is an escape-hatch type of situation and is simple to implement.
When you are rendering BlogListItem, we can pass a function, say onDelete, which will invoke getBlogPosts() to manually repopulate the blog posts from your server:
<BlogListItem key={post._id} post={post} onDelete={getBlogPosts} />
Then it is a matter of ensuring BlogListItem invokes onDelete() when deleting a blog post:
const handleDelete = async (event) => {
event.preventDefault();
const choice = window.confirm("Are you sure you want to delete this post?");
if (!choice) return;
await deleteBlogPost(post._id);
// Invoke the passed in `onDelete` function in component props
props.onDelete();
};
Solution 2: Delete a specific blog post by ID in the parent
Similar to the solution above, but ensure that you are passing a function from the parent that can delete a post by a specific ID (from the argument). This saves you an additional trip to the server.
In your component AdminBlogListView, define a function that can mutate the blogposts state by removing a blog post by ID. This can be done by leveraging functional updates:
const onDelete = (id) => {
setBlogposts((currentBlogPosts) => {
const foundBlogPostIndex = currentBlogPosts.findIndex(entry => entry._id === id);
// If we find the blog post with matching ID, remove it
if (foundBlogPostIndex !== -1) currentBlogPosts.splice(foundBlogPostIndex, 1);
return currentBlogPosts;
})
}
NOTE: The code above assumes that the blog post ID is stored in the _id key. I have simply inferred that from your code, since you have not shared the shape of the data.
Then in your BlogListItem component, it's the same logic as solution #1, but you need to pass the ID into it when invoking it:
const handleDelete = async (event) => {
event.preventDefault();
const choice = window.confirm("Are you sure you want to delete this post?");
if (!choice) return;
await deleteBlogPost(post._id);
// Invoke the passed in `onDelete` function in component props with post ID as an argument
props.onDelete(post._id);
};

How can I use a useState variable inside uesEffect?

I am trying to access the useState variable query in my function inside useEffect. I get the error React Hook useEffect has a missing dependency: 'query'. Either include it or remove the dependency array.
I think the problem is that im setting the (useState) query variable when the user types in the search bar and then I am trying to access this new query variable in the useEffect hook.
I want to fetch the api after I join the api url and the contents of query but after setQuery is executed which is after the user types in the search bar. How can I do this?
Thanks
Heres the code; notice the query variable.
import React, { useState, useEffect } from "react";
import Grid from '#mui/material/Grid';
import PaperCard from '../components/ResearchPaperCard';
const apiUrl = "http://127.0.0.1:8000/api/search/?search=";
function SearchedPapers(){
const [query, setQuery] = useState("");
const [apiData, setApiData] = useState([])
useEffect(() => {
const getFilteredItems = async (query) => {
let response = await fetch(apiUrl+query);
let papers = await response.json();
setApiData(papers);
if (!query) {
return papers
}
return papers;
}
getFilteredItems(query);
},[]);
console.log(apiData)
return (
<div className='SearchedPapers'>
<label>
Search
</label>
<input type='text' onChange={e => setQuery(e.target.value)}/>
<div>
{apiData.map((paper) => {
return (
<Grid key={paper.title}>
<PaperCard title={paper.title} abstract={paper.abstract}/>
</Grid>
)
})}
</div>
</div>
)
}
export default SearchedPapers;
Right now your useEffect is triggered each time your component mount so basically when someone reaches this screen.
At this time your query state is empty.
Try adding this :
useEffect(() => {
const getFilteredItems = async (query) => {
let response = await fetch(apiUrl+query);
let papers = await response.json();
setApiData(papers);
if (!query) {
return papers
}
return papers;
}
getFilteredItems(query);
},[query]) - - - - > here
By doing so, your useEffect will be triggered only and each time your query state changes
You should either include query in your dependency array i.e., second argument of your UseEffect hook or either remove. When it's(array) empty useEffect will only render once i.e., when your page render for first time but when removed useEffect runs both after the first render and after every update. When specified like you put 'query' in dependency array it will only run whenever there is change in 'query' state.

CoinGecko API data undefined after re-render React JS

Trying to render data from the CoinGekco API in my React component. It works on first render but if I leave the page or refresh, coin.market_data is undefined. I also tried passing coin to the useEffect() dependency array and that didn't work.
import React, { useEffect, useState } from "react";
import axios from "../utils/axios";
import CoinDetail from "./CoinDetail";
function CoinPagePage() {
const [coin, setCoin] = useState({});
useEffect(() => {
const getCoin = () => {
const coinid = window.location.pathname.split("/").splice(2).toString();
axios
.get(`/coins/${coinid}`)
.then((res) => {
setCoin(res.data);
console.log(res.data);
})
.catch((error) => console.log(error));
};
getCoin();
}, []);
return (
<div>
<CoinDetail current_price={coin.market_data.current_price.usd} />
</div>
);
}
export default CoinPagePage;
The GET request only happens when rendering the parent page. Re-rendering the child component will not run the fetch code again. Instead of passing current_price as a prop to your <CoinDetail> component, you could try passing coinid and doing the fetch inside your detail page.
That way, when the page is refreshed, the request will be executed again.
Edit
If you try to access a not existing property on an object, your application will crash. What you could do to prevent this from happening is checking if the request is done, before trying to access the property.
One way you could do this by setting the initial state value to null
const [coin, setCoin] = useState(null);
Then, above the main return, you could check if the value is null, if it is, return some sort of loading screen
if(coin === null) return <LoadingScreen />;
// main render
return (
<div>
<CoinDetail current_price={coin.market_data.current_price.usd} />
</div>
);
This way, when the fetch is done, the state gets updated and the page will re-render and show the updated content.

How to prevent state updates from a function, running an async call

so i have a bit of a weird problem i dont know how to solve.
In my code i have a custom hook with a bunch of functionality for a fetching a list
of train journeys. I have some useEffects to that keeps loading in new journeys untill the last journey of the day.
When i change route, while it is still loading in new journeys. I get the "changes to unmounted component" React error.
I understand that i get this error because the component is doing an async fetch that finishes after i've gone to a new page.
The problem i can't figure out is HOW do i prevent it from doing that? the "unmounted" error always occur on one of the 4 lines listed in the code snippet.
Mock of the code:
const [loading, setLoading] = useState(true);
const [journeys, setJourneys] = useState([]);
const [hasLaterDepartures, setHasLaterDepartures] = useState(true);
const getJourneys = async (date, journeys) => {
setLoading(true);
setHasLaterDepartures(true);
const selectedDateJourneys = await fetchJourney(date); // Fetch that returns 0-3 journeys
if (condition1) setHasLaterDepartures(false); // trying to update unmounted component
if (condition2) {
if (condition3) {
setJourneys(something1); // trying to update unmounted component
} else {
setJourneys(something2) // trying to update unmounted component
}
} else {
setJourneys(something3); // trying to update unmounted component
}
};
// useEffects for continous loading of journeys.
useEffect(() => {
if (!hasLaterDepartures) setLoading(false);
}, [hasLaterDepartures]);
useEffect(() => {
if (hasLaterDepartures && journeys.length > 0) {
const latestStart = ... // just a date
if (latestStart.addMinutes(5).isSameDay(latestStart)) {
getJourneys(latestStart.addMinutes(5), journeys);
} else {
setLoading(false);
}
}
}, [journeys]);
I can't use a variable like isMounted = true in the useEffect beacuse it would reach inside the if statement and reach a "setState" by the time i'm on another page.
Moving the entire call into a useEffect doesn't seem to work either. I am at a loss.
Create a variable called mounted with useRef, initialised as true. Then add an effect to set mounted.current to false when the component unmounts.
You can use mounted.current anywhere inside the component to see if it's mounted, and check that before setting any state.
useRef gives you a variable you can mutate but which doesn't cause a rerender.
When you use useEffect hook with action which can be done after component change you should also take care about clean effect when needed. Maybe example help you, also check this page.
useEffect(() => {
let isClosed = false
const fetchData = async () => {
const data = await response.json()
if ( !isClosed ) {
setState( data )
}
};
fetchData()
return () => {
isClosed = true
};
}, []);
In your use case, you probably want to create a Store that doesn't reload everytime you change route (client side).
Example of a store using useContext();
const MyStoreContext = createContext()
export function useMyStore() {
const context = useContext(MyStoreContext)
if (!context && typeof window !== 'undefined') {
throw new Error(`useMyStore must be used within a MyStoreContext`)
}
return context
}
export function MyStoreProvider(props) {
const [ myState, setMyState ] = useState()
//....whatever codes u doing with ur hook.
const exampleCustomFunction = () => {
return myState
}
const getAllRoutes = async (mydestination) => {
return await getAllMyRoutesFromApi(mydestination)
}
// you return all your "getter" and "setter" in value props so you can use them outside the store.
return <MyStoreContext.Provider value={{ myState, setMyState, exampleCustomFunction, getAllRoutes }}>{props.children}</MyStoreContext.Provider>
}
You will wrap the store around your entire App, e.g.
<MyStoreProvider>
<App />
</MyStoreProvider>
In your page where you want to use your hook, you can do
const { myState, setMyState, exampleCustomFunction, getAllRoutes } = useMyStore()
const onClick = async () => getAllRouters(mydestination)
Considering if you have client side routing (not server side), this doesn't get reloaded every time you change your route.

Component is rendering twice

I am following a tutorial and just learned about useEffect.
The issue I am having is that I want to get result.hours[0].is_open_now. I've discovered that the component renders twice, and since hours is undefined at first, it fails.
Is there a way to make this only run once.
const ResultsShowScreen = ({ navigation }) => {
const id = navigation.getParam('id');
const [result, setResult] = React.useState({})
const getResult = async (id) => {
const response = await yelp.get(`/${id}`)
setResult(response.data)
}
useEffect(() => {
getResult(id)
}, [])
if (!result || result.length <= 0) {
return null
}
const {name,
display_address,
price,
display_phone,
photos,
review_count,
rating,
hours
} = result
if (hours && hours.length > 0 ) {
const is_open_now = hours[0].is_open_now
console.log('running')
} else {
console.log('nooooo')
}
return (
<View>
<Text style={styles.title}>{name}</Text>
<Text>Price: {price}</Text>
<Text>Rating: {rating}</Text>
<Text>Reviews: {review_count}</Text>
<Text>Phone: {display_phone}</Text>
<Text>Is Open Now: </Text>
</View>
)
}
export default ResultsShowScreen
Following your code you can not prevent useEffect render twice because following react hook lifecycle,
If you’re familiar with React class lifecycle methods, you can think
of useEffect Hook as componentDidMount, componentDidUpdate, and
componentWillUnmount combined.
That's mean useEffect will be executed after component render the first time (Your code fiirst time is null) and executed more time if the second arg has values.
In your useEffect, you call setState to set new state and component auto render if state change. So your component will be rendered at least twice.

Categories

Resources